From 087ec11e6abc61acde4e2860f8ee363f3daba578 Mon Sep 17 00:00:00 2001 From: Andrew Grieve Date: Thu, 5 Feb 2015 20:46:42 -0500 Subject: [PATCH] CB-8510 Create a new abstraction for sharing common logic of WebView engines Having CordovaWebViewImpl separate from CordovaWebViewEngine is helpful because now each webview doesn't have to re-implement non-webview-specific featrues. e.g.: 1. load timeout 2. keyboard events 3. showCustomView 4. lifecycle events Moved AndroidWebView into its own package to ensure that it doesn't use any package-private symbols (since plugins cannot use them). --- .../org/apache/cordova/AndroidWebView.java | 791 ------------------ .../org/apache/cordova/CordovaActivity.java | 25 +- .../org/apache/cordova/CordovaUriHelper.java | 84 -- .../org/apache/cordova/CordovaWebView.java | 30 +- .../apache/cordova/CordovaWebViewEngine.java | 82 ++ .../apache/cordova/CordovaWebViewImpl.java | 645 ++++++++++++++ .../cordova/NativeToJsMessageQueue.java | 8 +- .../src/org/apache/cordova/PluginManager.java | 4 +- .../SystemCookieManager.java} | 9 +- .../SystemExposedJsApi.java} | 9 +- .../SystemWebChromeClient.java} | 42 +- .../apache/cordova/engine/SystemWebView.java | 88 ++ .../SystemWebViewClient.java} | 118 +-- .../cordova/engine/SystemWebViewEngine.java | 312 +++++++ .../cordova/test/CordovaActivityTest.java | 6 +- .../cordova/test/InflateLayoutTest.java | 4 +- test/res/layout/main.xml | 2 +- .../test/CordovaWebViewTestActivity.java | 13 +- .../org/apache/cordova/test/userwebview.java | 25 +- 19 files changed, 1265 insertions(+), 1032 deletions(-) delete mode 100755 framework/src/org/apache/cordova/AndroidWebView.java delete mode 100644 framework/src/org/apache/cordova/CordovaUriHelper.java create mode 100644 framework/src/org/apache/cordova/CordovaWebViewEngine.java create mode 100644 framework/src/org/apache/cordova/CordovaWebViewImpl.java rename framework/src/org/apache/cordova/{AndroidCookieManager.java => engine/SystemCookieManager.java} (90%) rename framework/src/org/apache/cordova/{AndroidExposedJsApi.java => engine/SystemExposedJsApi.java} (89%) rename framework/src/org/apache/cordova/{AndroidChromeClient.java => engine/SystemWebChromeClient.java} (88%) create mode 100644 framework/src/org/apache/cordova/engine/SystemWebView.java rename framework/src/org/apache/cordova/{AndroidWebViewClient.java => engine/SystemWebViewClient.java} (78%) create mode 100755 framework/src/org/apache/cordova/engine/SystemWebViewEngine.java diff --git a/framework/src/org/apache/cordova/AndroidWebView.java b/framework/src/org/apache/cordova/AndroidWebView.java deleted file mode 100755 index 60296a9a..00000000 --- a/framework/src/org/apache/cordova/AndroidWebView.java +++ /dev/null @@ -1,791 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -package org.apache.cordova; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.webkit.WebBackForwardList; -import android.webkit.WebHistoryItem; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebSettings.LayoutAlgorithm; -import android.webkit.WebViewClient; -import android.widget.FrameLayout; - - -/* - * This class is our web view. - * - * @see WebView guide - * @see WebView - */ -public class AndroidWebView extends WebView implements CordovaWebView { - - public static final String TAG = "AndroidWebView"; - - private HashSet boundKeyCodes = new HashSet(); - - PluginManager pluginManager; - AndroidCookieManager cookieManager; - - private BroadcastReceiver receiver; - - - /** Activities and other important classes **/ - private CordovaInterface cordova; - AndroidWebViewClient viewClient; - private AndroidChromeClient chromeClient; - - // Flag to track that a loadUrl timeout occurred - int loadUrlTimeout = 0; - - private long lastMenuEventTime = 0; - - private NativeToJsMessageQueue nativeToJsMessageQueue; - CordovaBridge bridge; - - /** custom view created by the browser (a video player for example) */ - private View mCustomView; - private WebChromeClient.CustomViewCallback mCustomViewCallback; - - private CordovaResourceApi resourceApi; - private CordovaPreferences preferences; - private CoreAndroid appPlugin; - private CordovaUriHelper helper; - // The URL passed to loadUrl(), not necessarily the URL of the current page. - String loadedUrl; - - static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER = - new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - Gravity.CENTER); - - /** Used when created via reflection. */ - public AndroidWebView(Context context) { - this(context, null); - } - - /** Required to allow view to be used within XML layouts. */ - public AndroidWebView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - // Use two-phase init so that the control will work with XML layouts. - @Override - public void init(final CordovaInterface cordova, List pluginEntries, - CordovaPreferences preferences) { - if (this.cordova != null) { - throw new IllegalStateException(); - } - this.cordova = cordova; - this.preferences = preferences; - this.helper = new CordovaUriHelper(cordova, this); - - pluginManager = new PluginManager(this, this.cordova, pluginEntries); - cookieManager = new AndroidCookieManager(this); - resourceApi = new CordovaResourceApi(this.getContext(), pluginManager); - nativeToJsMessageQueue = new NativeToJsMessageQueue(); - nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.NoOpBridgeMode()); - nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.LoadUrlBridgeMode(this, cordova)); - nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode.OnlineEventsBridgeModeDelegate() { - @Override - public void setNetworkAvailable(boolean value) { - AndroidWebView.this.setNetworkAvailable(value); - } - - @Override - public void runOnUiThread(Runnable r) { - cordova.getActivity().runOnUiThread(r); - } - })); - bridge = new CordovaBridge(pluginManager, nativeToJsMessageQueue); - initWebViewSettings(); - pluginManager.addService(CoreAndroid.PLUGIN_NAME, CoreAndroid.class.getCanonicalName()); - pluginManager.init(); - - if (this.viewClient == null) { - setWebViewClient(new AndroidWebViewClient(cordova, this)); - } - - if (this.chromeClient == null) { - setWebChromeClient(new AndroidChromeClient(cordova, this)); - } - - exposeJsInterface(); - - if (preferences.getBoolean("DisallowOverscroll", false)) { - setOverScrollMode(View.OVER_SCROLL_NEVER); - } - } - - @SuppressLint("SetJavaScriptEnabled") - @SuppressWarnings("deprecation") - private void initWebViewSettings() { - this.setInitialScale(0); - this.setVerticalScrollBarEnabled(false); - final WebSettings settings = this.getSettings(); - settings.setJavaScriptEnabled(true); - settings.setJavaScriptCanOpenWindowsAutomatically(true); - settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL); - - // Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2) - try { - Method gingerbread_getMethod = WebSettings.class.getMethod("setNavDump", new Class[] { boolean.class }); - - String manufacturer = android.os.Build.MANUFACTURER; - Log.d(TAG, "CordovaWebView is running on device made by: " + manufacturer); - if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB && - android.os.Build.MANUFACTURER.contains("HTC")) - { - gingerbread_getMethod.invoke(settings, true); - } - } catch (NoSuchMethodException e) { - Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8"); - } catch (IllegalArgumentException e) { - Log.d(TAG, "Doing the NavDump failed with bad arguments"); - } catch (IllegalAccessException e) { - Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore"); - } catch (InvocationTargetException e) { - Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore."); - } - - //We don't save any form data in the application - settings.setSaveFormData(false); - settings.setSavePassword(false); - - // Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist - // while we do this - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - Level16Apis.enableUniversalAccess(settings); - } - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { - Level17Apis.setMediaPlaybackRequiresUserGesture(settings, false); - } - // Enable database - // We keep this disabled because we use or shim to get around DOM_EXCEPTION_ERROR_16 - String databasePath = getContext().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath(); - settings.setDatabaseEnabled(true); - settings.setDatabasePath(databasePath); - - - //Determine whether we're in debug or release mode, and turn on Debugging! - ApplicationInfo appInfo = getContext().getApplicationContext().getApplicationInfo(); - if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 && - android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { - enableRemoteDebugging(); - } - - settings.setGeolocationDatabasePath(databasePath); - - // Enable DOM storage - settings.setDomStorageEnabled(true); - - // Enable built-in geolocation - settings.setGeolocationEnabled(true); - - // Enable AppCache - // Fix for CB-2282 - settings.setAppCacheMaxSize(5 * 1048576); - settings.setAppCachePath(databasePath); - settings.setAppCacheEnabled(true); - - // Fix for CB-1405 - // Google issue 4641 - settings.getUserAgentString(); - - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); - if (this.receiver == null) { - this.receiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - settings.getUserAgentString(); - } - }; - getContext().registerReceiver(this.receiver, intentFilter); - } - // end CB-1405 - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void enableRemoteDebugging() { - try { - WebView.setWebContentsDebuggingEnabled(true); - } catch (IllegalArgumentException e) { - Log.d(TAG, "You have one job! To turn on Remote Web Debugging! YOU HAVE FAILED! "); - e.printStackTrace(); - } - } - - private void exposeJsInterface() { - if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) { - Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old."); - // Bug being that Java Strings do not get converted to JS strings automatically. - // This isn't hard to work-around on the JS side, but it's easier to just - // use the prompt bridge instead. - return; - } - AndroidExposedJsApi exposedJsApi = new AndroidExposedJsApi(bridge); - this.addJavascriptInterface(exposedJsApi, "_cordovaNative"); - } - - @Override - public void setWebViewClient(WebViewClient client) { - this.viewClient = (AndroidWebViewClient)client; - super.setWebViewClient(client); - } - - @Override - public void setWebChromeClient(WebChromeClient client) { - this.chromeClient = (AndroidChromeClient)client; - super.setWebChromeClient(client); - } - - /** - * Load the url into the webview. - */ - @Override - public void loadUrl(String url) { - this.loadUrlIntoView(url, true); - } - - /** - * Load the url into the webview. - */ - @Override - public void loadUrlIntoView(final String url, boolean recreatePlugins) { - if (url.equals("about:blank") || url.startsWith("javascript:")) { - this.loadUrlNow(url); - return; - } - - LOG.d(TAG, ">>> loadUrl(" + url + ")"); - recreatePlugins = recreatePlugins || (loadedUrl == null); - - if (recreatePlugins) { - // Don't re-initialize on first load. - if (loadedUrl != null) { - this.pluginManager.init(); - } - this.loadedUrl = url; - } - - // Create a timeout timer for loadUrl - final AndroidWebView me = this; - final int currentLoadUrlTimeout = me.loadUrlTimeout; - final int loadUrlTimeoutValue = preferences.getInteger("LoadUrlTimeoutValue", 20000); - - // Timeout error method - final Runnable loadError = new Runnable() { - public void run() { - me.stopLoading(); - LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!"); - if (viewClient != null) { - viewClient.onReceivedError(AndroidWebView.this, -6, "The connection to the server was unsuccessful.", url); - } - } - }; - - // Timeout timer method - final Runnable timeoutCheck = new Runnable() { - public void run() { - try { - synchronized (this) { - wait(loadUrlTimeoutValue); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - - // If timeout, then stop loading and handle error - if (me.loadUrlTimeout == currentLoadUrlTimeout) { - me.cordova.getActivity().runOnUiThread(loadError); - } - } - }; - - // Load url - this.cordova.getActivity().runOnUiThread(new Runnable() { - public void run() { - cordova.getThreadPool().execute(timeoutCheck); - me.loadUrlNow(url); - } - }); - } - - /** - * Load URL in webview. - */ - private void loadUrlNow(String url) { - if (LOG.isLoggable(LOG.DEBUG) && !url.startsWith("javascript:")) { - LOG.d(TAG, ">>> loadUrlNow()"); - } - if (url.startsWith("javascript:") || pluginManager.shouldAllowNavigation(url)) { - super.loadUrl(url); - } - } - - @Override - public void stopLoading() { - //viewClient.isCurrentlyLoading = false; - super.stopLoading(); - } - - public void onScrollChanged(int l, int t, int oldl, int oldt) - { - super.onScrollChanged(l, t, oldl, oldt); - //We should post a message that the scroll changed - ScrollEvent myEvent = new ScrollEvent(l, t, oldl, oldt, this); - pluginManager.postMessage("onScrollChanged", myEvent); - } - - /** - * Send JavaScript statement back to JavaScript. - * (This is a convenience method) - */ - public void sendJavascript(String statement) { - nativeToJsMessageQueue.addJavaScript(statement); - } - - /** - * Send a plugin result back to JavaScript. - */ - public void sendPluginResult(PluginResult result, String callbackId) { - nativeToJsMessageQueue.addPluginResult(result, callbackId); - } - - /** - * Go to previous page in history. (We manage our own history) - * - * @return true if we went back, false if we are already at top - */ - public boolean backHistory() { - - // Check webview first to see if there is a history - // This is needed to support curPage#diffLink, since they are added to appView's history, but not our history url array (JQMobile behavior) - if (super.canGoBack()) { - printBackForwardList(); - super.goBack(); - - return true; - } - return false; - } - - - /** - * Load the specified URL in the Cordova webview or a new browser instance. - * - * NOTE: If openExternal is false, only URLs listed in whitelist can be loaded. - * - * @param url The url to load. - * @param openExternal Load url in browser instead of Cordova webview. - * @param clearHistory Clear the history stack, so new page becomes top of history - * @param params Parameters for new app - */ - public void showWebPage(String url, boolean openExternal, boolean clearHistory, Map params) { - LOG.d(TAG, "showWebPage(%s, %b, %b, HashMap", url, openExternal, clearHistory); - - // If clearing history - if (clearHistory) { - this.clearHistory(); - } - - // If loading into our webview - if (!openExternal) { - - // Make sure url is in whitelist - if (pluginManager.shouldAllowNavigation(url)) { - // TODO: What about params? - // Load new URL - loadUrlIntoView(url, true); - return; - } - // Load in default viewer if not - LOG.w(TAG, "showWebPage: Cannot load URL into webview since it is not in white list. Loading into browser instead. (URL=" + url + ")"); - } - try { - // Omitting the MIME type for file: URLs causes "No Activity found to handle Intent". - // Adding the MIME type to http: URLs causes them to not be handled by the downloader. - Intent intent = new Intent(Intent.ACTION_VIEW); - Uri uri = Uri.parse(url); - if ("file".equals(uri.getScheme())) { - intent.setDataAndType(uri, resourceApi.getMimeType(uri)); - } else { - intent.setData(uri); - } - cordova.getActivity().startActivity(intent); - } catch (android.content.ActivityNotFoundException e) { - LOG.e(TAG, "Error loading url " + url, e); - } - } - - /* - * onKeyDown - */ - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) - { - if(boundKeyCodes.contains(keyCode)) - { - if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { - sendJavascriptEvent("volumedownbutton"); - return true; - } - else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - sendJavascriptEvent("volumeupbutton"); - return true; - } - else - { - return super.onKeyDown(keyCode, event); - } - } - else if(keyCode == KeyEvent.KEYCODE_BACK) - { - return !(this.startOfHistory()) || isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK); - - } - else if(keyCode == KeyEvent.KEYCODE_MENU) - { - //How did we get here? Is there a childView? - View childView = this.getFocusedChild(); - if(childView != null) - { - //Make sure we close the keyboard if it's present - InputMethodManager imm = (InputMethodManager) cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(childView.getWindowToken(), 0); - cordova.getActivity().openOptionsMenu(); - return true; - } else { - return super.onKeyDown(keyCode, event); - } - } - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) - { - // If back key - if (keyCode == KeyEvent.KEYCODE_BACK) { - // A custom view is currently displayed (e.g. playing a video) - if(mCustomView != null) { - this.hideCustomView(); - return true; - } else { - // The webview is currently displayed - // If back key is bound, then send event to JavaScript - if (isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK)) { - sendJavascriptEvent("backbutton"); - return true; - } else { - // If not bound - // Go to previous page in webview if it is possible to go back - if (this.backHistory()) { - return true; - } - // If not, then invoke default behavior - } - } - } - // Legacy - else if (keyCode == KeyEvent.KEYCODE_MENU) { - if (this.lastMenuEventTime < event.getEventTime()) { - sendJavascriptEvent("menubutton"); - } - this.lastMenuEventTime = event.getEventTime(); - return super.onKeyUp(keyCode, event); - } - // If search key - else if (keyCode == KeyEvent.KEYCODE_SEARCH) { - sendJavascriptEvent("searchbutton"); - return true; - } - - //Does webkit change this behavior? - return super.onKeyUp(keyCode, event); - } - - private void sendJavascriptEvent(String event) { - if (appPlugin == null) { - appPlugin = (CoreAndroid)this.pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME); - } - - if (appPlugin == null) { - LOG.w(TAG, "Unable to fire event without existing plugin"); - return; - } - appPlugin.fireJavascriptEvent(event); - } - - @Override - public void setButtonPlumbedToJs(int keyCode, boolean override) { - switch (keyCode) { - case KeyEvent.KEYCODE_VOLUME_DOWN: - case KeyEvent.KEYCODE_VOLUME_UP: - case KeyEvent.KEYCODE_BACK: - // TODO: Why are search and menu buttons handled separately? - if (override) { - boundKeyCodes.add(keyCode); - } else { - boundKeyCodes.remove(keyCode); - } - return; - default: - throw new IllegalArgumentException("Unsupported keycode: " + keyCode); - } - } - - @Override - public boolean isButtonPlumbedToJs(int keyCode) - { - return boundKeyCodes.contains(keyCode); - } - - @Override - public void handlePause(boolean keepRunning) - { - LOG.d(TAG, "Handle the pause"); - // Send pause event to JavaScript - sendJavascriptEvent("pause"); - - // Forward to plugins - if (this.pluginManager != null) { - this.pluginManager.onPause(keepRunning); - } - - // If app doesn't want to run in background - if (!keepRunning) { - // Pause JavaScript timers. This affects all webviews within the app! - this.pauseTimers(); - } - } - - @Override - public void handleResume(boolean keepRunning) - { - // Resume JavaScript timers. This affects all webviews within the app! - this.resumeTimers(); - - sendJavascriptEvent("resume"); - - // Forward to plugins - if (this.pluginManager != null) { - this.pluginManager.onResume(keepRunning); - } - } - - public void handleDestroy() - { - // Cancel pending timeout timer. - loadUrlTimeout++; - - // Load blank page so that JavaScript onunload is called - this.loadUrl("about:blank"); - - //Remove last AlertDialog - this.chromeClient.destroyLastDialog(); - - // Forward to plugins - if (this.pluginManager != null) { - this.pluginManager.onDestroy(); - } - - // unregister the receiver - if (this.receiver != null) { - try { - getContext().unregisterReceiver(this.receiver); - } catch (Exception e) { - Log.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e); - } - } - } - - public void onNewIntent(Intent intent) - { - //Forward to plugins - if (this.pluginManager != null) { - this.pluginManager.onNewIntent(intent); - } - } - - // Wrapping these functions in their own class prevents warnings in adb like: - // VFY: unable to resolve virtual method 285: Landroid/webkit/WebSettings;.setAllowUniversalAccessFromFileURLs - @TargetApi(16) - private static class Level16Apis { - static void enableUniversalAccess(WebSettings settings) { - settings.setAllowUniversalAccessFromFileURLs(true); - } - } - - @TargetApi(17) - private static final class Level17Apis { - static void setMediaPlaybackRequiresUserGesture(WebSettings settings, boolean value) { - settings.setMediaPlaybackRequiresUserGesture(value); - } - } - public void printBackForwardList() { - WebBackForwardList currentList = this.copyBackForwardList(); - int currentSize = currentList.getSize(); - for(int i = 0; i < currentSize; ++i) - { - WebHistoryItem item = currentList.getItemAtIndex(i); - String url = item.getUrl(); - LOG.d(TAG, "The URL at index: " + Integer.toString(i) + " is " + url ); - } - } - - - //Can Go Back is BROKEN! - public boolean startOfHistory() - { - WebBackForwardList currentList = this.copyBackForwardList(); - WebHistoryItem item = currentList.getItemAtIndex(0); - if( item!=null){ // Null-fence in case they haven't called loadUrl yet (CB-2458) - String url = item.getUrl(); - String currentUrl = this.getUrl(); - LOG.d(TAG, "The current URL is: " + currentUrl); - LOG.d(TAG, "The URL at item 0 is: " + url); - return currentUrl.equals(url); - } - return false; - } - - public void showCustomView(View view, WebChromeClient.CustomViewCallback callback) { - // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0 - Log.d(TAG, "showing Custom View"); - // if a view already exists then immediately terminate the new one - if (mCustomView != null) { - callback.onCustomViewHidden(); - return; - } - - // Store the view and its callback for later (to kill it properly) - mCustomView = view; - mCustomViewCallback = callback; - - // Add the custom view to its container. - ViewGroup parent = (ViewGroup) this.getParent(); - parent.addView(view, COVER_SCREEN_GRAVITY_CENTER); - - // Hide the content view. - this.setVisibility(View.GONE); - - // Finally show the custom view container. - parent.setVisibility(View.VISIBLE); - parent.bringToFront(); - } - - public void hideCustomView() { - // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0 - Log.d(TAG, "Hiding Custom View"); - if (mCustomView == null) return; - - // Hide the custom view. - mCustomView.setVisibility(View.GONE); - - // Remove the custom view from its container. - ViewGroup parent = (ViewGroup) this.getParent(); - parent.removeView(mCustomView); - mCustomView = null; - mCustomViewCallback.onCustomViewHidden(); - - // Show the content view. - this.setVisibility(View.VISIBLE); - } - - /** - * if the video overlay is showing then we need to know - * as it effects back button handling - * - * @return true if custom view is showing - */ - public boolean isCustomViewShowing() { - return mCustomView != null; - } - - public WebBackForwardList restoreState(Bundle savedInstanceState) - { - WebBackForwardList myList = super.restoreState(savedInstanceState); - Log.d(TAG, "WebView restoration crew now restoring!"); - //Initialize the plugin manager once more - this.pluginManager.init(); - return myList; - } - - public CordovaResourceApi getResourceApi() { - return resourceApi; - } - - void onPageReset() { - boundKeyCodes.clear(); - pluginManager.onReset(); - bridge.reset(); - } - - @Override - public PluginManager getPluginManager() { - return this.pluginManager; - } - - @Override - public View getView() { - return this; - } - - @Override - public CordovaPreferences getPreferences() { - return preferences; - } - - @Override - public ICordovaCookieManager getCookieManager() { - return cookieManager; - } - - @Override - public Object postMessage(String id, Object data) { - return pluginManager.postMessage(id, data); - } -} diff --git a/framework/src/org/apache/cordova/CordovaActivity.java b/framework/src/org/apache/cordova/CordovaActivity.java index 9aa5e1f0..5f68333f 100755 --- a/framework/src/org/apache/cordova/CordovaActivity.java +++ b/framework/src/org/apache/cordova/CordovaActivity.java @@ -18,16 +18,15 @@ */ package org.apache.cordova; -import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Locale; +import org.apache.cordova.engine.SystemWebViewEngine; import org.json.JSONException; import org.json.JSONObject; import android.app.Activity; import android.app.AlertDialog; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; @@ -97,7 +96,6 @@ public class CordovaActivity extends Activity { protected ArrayList pluginEntries; protected CordovaInterfaceImpl cordovaInterface; - /** * Called when the activity is first created. */ @@ -138,8 +136,9 @@ public class CordovaActivity extends Activity { protected void init() { appView = makeWebView(); createViews(); - //TODO: Add null check against CordovaInterfaceImpl, since this can be fragile - appView.init(cordovaInterface, pluginEntries, preferences); + if (!appView.isInitialized()) { + appView.init(cordovaInterface, pluginEntries, preferences); + } cordovaInterface.setPluginManager(appView.getPluginManager()); // Wire the hardware volume controls to control media if desired. @@ -198,16 +197,12 @@ public class CordovaActivity extends Activity { * Override this to customize the webview that is used. */ protected CordovaWebView makeWebView() { - String webViewClassName = preferences.getString("webView", AndroidWebView.class.getCanonicalName()); - CordovaWebView ret; - try { - Class webViewClass = Class.forName(webViewClassName); - Constructor constructor = webViewClass.getConstructor(Context.class); - ret = (CordovaWebView) constructor.newInstance((Context)this); - return ret; - } catch (Exception e) { - throw new RuntimeException("Failed to create webview. ", e); - } + return new CordovaWebViewImpl(this, makeWebViewEngine()); + } + + protected CordovaWebViewEngine makeWebViewEngine() { + String className = preferences.getString("webview", SystemWebViewEngine.class.getCanonicalName()); + return CordovaWebViewImpl.createEngine(className, this, preferences); } protected CordovaInterfaceImpl makeCordovaInterface() { diff --git a/framework/src/org/apache/cordova/CordovaUriHelper.java b/framework/src/org/apache/cordova/CordovaUriHelper.java deleted file mode 100644 index 5685c705..00000000 --- a/framework/src/org/apache/cordova/CordovaUriHelper.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -package org.apache.cordova; - -import android.annotation.TargetApi; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.util.Log; -import android.webkit.WebView; - -public class CordovaUriHelper { - - private static final String TAG = "CordovaUriHelper"; - - private CordovaWebView appView; - private CordovaInterface cordova; - - public CordovaUriHelper(CordovaInterface cdv, CordovaWebView webView) - { - appView = webView; - cordova = cdv; - } - - /** - * Give the host application a chance to take over the control when a new url - * is about to be loaded in the current WebView. - * - * This method implements the default whitelist policy when no plugins override - * the whitelist methods: - * Internal urls on file:// or data:// that do not contain "app_webview" are allowed for navigation - * External urls are not allowed. - * - * @param url The url to be loaded. - * @return true to override, false for default behavior - */ - @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) - public boolean shouldOverrideUrlLoading(String url) { - // Give plugins the chance to handle the url - if (appView.getPluginManager().shouldAllowNavigation(url)) { - // Allow internal navigation - return false; - } - if (appView.getPluginManager().shouldOpenExternalUrl(url)) { - // Do nothing other than what the plugins wanted. - // If any returned false, then the request was either blocked - // completely, or handled out-of-band by the plugin. If they all - // returned true, then we should open the URL here. - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - intent.addCategory(Intent.CATEGORY_BROWSABLE); - intent.setComponent(null); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { - intent.setSelector(null); - } - this.cordova.getActivity().startActivity(intent); - return true; - } catch (android.content.ActivityNotFoundException e) { - Log.e(TAG, "Error loading url " + url, e); - } - return true; - } - // Block by default - return true; - } -} diff --git a/framework/src/org/apache/cordova/CordovaWebView.java b/framework/src/org/apache/cordova/CordovaWebView.java index 2a27a33e..d3071f41 100644 --- a/framework/src/org/apache/cordova/CordovaWebView.java +++ b/framework/src/org/apache/cordova/CordovaWebView.java @@ -16,19 +16,27 @@ */ package org.apache.cordova; -import java.util.Map; import java.util.List; +import java.util.Map; import android.content.Context; import android.content.Intent; import android.view.View; import android.webkit.WebChromeClient.CustomViewCallback; +/** + * Main interface for interacting with a Cordova webview - implemented by CordovaWebViewImpl. + * This is an interface so that it can be easily mocked in tests. + * Methods may be added to this interface without a major version bump, as plugins & embedders + * are not expected to implement it. + */ public interface CordovaWebView { public static final String CORDOVA_VERSION = "4.0.0-dev"; void init(CordovaInterface cordova, List pluginEntries, CordovaPreferences preferences); + boolean isInitialized(); + View getView(); void loadUrlIntoView(String url, boolean recreatePlugins); @@ -37,6 +45,10 @@ public interface CordovaWebView { boolean canGoBack(); + void clearCache(); + + /** Use parameter-less overload */ + @Deprecated void clearCache(boolean b); void clearHistory(); @@ -75,7 +87,17 @@ public interface CordovaWebView { @Deprecated void sendJavascript(String statememt); - void showWebPage(String errorUrl, boolean b, boolean c, Map params); + /** + * Load the specified URL in the Cordova webview or a new browser instance. + * + * NOTE: If openExternal is false, only whitelisted URLs can be loaded. + * + * @param url The url to load. + * @param openExternal Load url in browser instead of Cordova webview. + * @param clearHistory Clear the history stack, so new page becomes top of history + * @param params Parameters for new app + */ + void showWebPage(String url, boolean openExternal, boolean clearHistory, Map params); /** * Deprecated in 4.0.0. Use your own View-toggling logic. @@ -103,12 +125,10 @@ public interface CordovaWebView { void sendPluginResult(PluginResult cr, String callbackId); PluginManager getPluginManager(); - + CordovaWebViewEngine getEngine(); CordovaPreferences getPreferences(); ICordovaCookieManager getCookieManager(); - void setNetworkAvailable(boolean online); - String getUrl(); // TODO: Work on deleting these by removing refs from plugins. diff --git a/framework/src/org/apache/cordova/CordovaWebViewEngine.java b/framework/src/org/apache/cordova/CordovaWebViewEngine.java new file mode 100644 index 00000000..03f697cc --- /dev/null +++ b/framework/src/org/apache/cordova/CordovaWebViewEngine.java @@ -0,0 +1,82 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import android.view.KeyEvent; +import android.view.View; + +/** + * Interfcae for all Cordova engines. + * No methods will be added to this class (in order to be compatible with existing engines). + * Instead, we will create a new interface: e.g. CordovaWebViewEngineV2 + */ +public interface CordovaWebViewEngine { + void init(CordovaWebView parentWebView, CordovaInterface cordova, Client client, + CordovaResourceApi resourceApi, PluginManager pluginManager, + NativeToJsMessageQueue nativeToJsMessageQueue); + + CordovaWebView getCordovaWebView(); + ICordovaCookieManager getCookieManager(); + View getView(); + + void loadUrl(String url, boolean clearNavigationStack); + + void stopLoading(); + + /** Return the currently loaded URL */ + String getUrl(); + + void clearCache(); + + /** After calling clearHistory(), canGoBack() should be false. */ + void clearHistory(); + + boolean canGoBack(); + + /** Returns whether a navigation occurred */ + boolean goBack(); + + /** Pauses / resumes the WebView's event loop. */ + void setPaused(boolean value); + + /** Clean up all resources associated with the WebView. */ + void destroy(); + + /** + * Used to retrieve the associated CordovaWebView given a View without knowing the type of Engine. + * E.g. ((CordovaWebView.EngineView)activity.findViewById(android.R.id.webView)).getCordovaWebView(); + */ + public interface EngineView { + CordovaWebView getCordovaWebView(); + } + + /** + * Contains methods that an engine uses to communicate with the parent CordovaWebView. + * Methods may be added in future cordova versions, but never removed. + */ + public interface Client { + Boolean onKeyDown(int keyCode, KeyEvent event); + Boolean onKeyUp(int keyCode, KeyEvent event); + boolean shouldOverrideUrlLoading(String url); + void clearLoadTimeoutTimer(); + void onPageStarted(String newUrl); + void onReceivedError(int errorCode, String description, String failingUrl); + void onPageFinishedLoading(String url); + } +} diff --git a/framework/src/org/apache/cordova/CordovaWebViewImpl.java b/framework/src/org/apache/cordova/CordovaWebViewImpl.java new file mode 100644 index 00000000..aa816fbe --- /dev/null +++ b/framework/src/org/apache/cordova/CordovaWebViewImpl.java @@ -0,0 +1,645 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.webkit.WebChromeClient; +import android.widget.FrameLayout; + +import org.apache.cordova.engine.SystemWebViewEngine; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Main class for interacting with a Cordova webview. Manages plugins, events, and a CordovaWebViewEngine. + * Class uses two-phase initialization. You must call init() before calling any other methods. + */ +public class CordovaWebViewImpl implements CordovaWebView { + + public static final String TAG = "CordovaWebViewImpl"; + + // Public for backwards-compatibility :( + public PluginManager pluginManager; + + protected CordovaWebViewEngine engine; + private CordovaInterface cordova; + + // Flag to track that a loadUrl timeout occurred + private int loadUrlTimeout = 0; + + private CordovaResourceApi resourceApi; + private CordovaPreferences preferences; + private CoreAndroid appPlugin; + private NativeToJsMessageQueue nativeToJsMessageQueue; + private EngineClient engineClient = new EngineClient(); + private Context context; + + // The URL passed to loadUrl(), not necessarily the URL of the current page. + String loadedUrl; + + /** custom view created by the browser (a video player for example) */ + private View mCustomView; + private WebChromeClient.CustomViewCallback mCustomViewCallback; + + private Set boundKeyCodes = new HashSet(); + + public static CordovaWebViewEngine createEngine(String className, Context context, CordovaPreferences preferences) { + try { + Class webViewClass = Class.forName(className); + Constructor constructor = webViewClass.getConstructor(Context.class, CordovaPreferences.class); + return (CordovaWebViewEngine) constructor.newInstance(context, preferences); + } catch (Exception e) { + throw new RuntimeException("Failed to create webview. ", e); + } + } + + public CordovaWebViewImpl(Context context) { + this(context, null); + } + public CordovaWebViewImpl(Context context, CordovaWebViewEngine cordovaWebViewEngine) { + this.context = context; + this.engine = cordovaWebViewEngine; + } + // Convenience method for when creating programmatically (not from Config.xml). + public void init(CordovaInterface cordova) { + init(cordova, new ArrayList(), new CordovaPreferences()); + } + + @Override + public void init(CordovaInterface cordova, List pluginEntries, CordovaPreferences preferences) { + if (this.cordova != null) { + throw new IllegalStateException(); + } + // Happens only when not using CordovaActivity. Usually, engine is set in the constructor. + if (engine == null) { + String className = preferences.getString("webView", SystemWebViewEngine.class.getCanonicalName()); + engine = createEngine(className, context, preferences); + } + this.cordova = cordova; + this.preferences = preferences; + pluginManager = new PluginManager(this, this.cordova, pluginEntries); + resourceApi = new CordovaResourceApi(engine.getView().getContext(), pluginManager); + nativeToJsMessageQueue = new NativeToJsMessageQueue(); + nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.NoOpBridgeMode()); + nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.LoadUrlBridgeMode(engine, cordova)); + + if (preferences.getBoolean("DisallowOverscroll", false)) { + engine.getView().setOverScrollMode(View.OVER_SCROLL_NEVER); + } + engine.init(this, cordova, engineClient, resourceApi, pluginManager, nativeToJsMessageQueue); + // This isn't enforced by the compiler, so assert here. + assert engine.getView() instanceof CordovaWebViewEngine.EngineView; + + pluginManager.addService(CoreAndroid.PLUGIN_NAME, "org.apache.cordova.CoreAndroid"); + pluginManager.init(); + } + + @Override + public boolean isInitialized() { + return cordova != null; + } + + @Override + public void loadUrlIntoView(final String url, boolean recreatePlugins) { + LOG.d(TAG, ">>> loadUrl(" + url + ")"); + if (url.equals("about:blank") || url.startsWith("javascript:")) { + engine.loadUrl(url, false); + return; + } + + recreatePlugins = recreatePlugins || (loadedUrl == null); + + if (recreatePlugins) { + // Don't re-initialize on first load. + if (loadedUrl != null) { + pluginManager.init(); + } + loadedUrl = url; + } + + // Create a timeout timer for loadUrl + final int currentLoadUrlTimeout = loadUrlTimeout; + final int loadUrlTimeoutValue = preferences.getInteger("LoadUrlTimeoutValue", 20000); + + // Timeout error method + final Runnable loadError = new Runnable() { + public void run() { + stopLoading(); + LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!"); + + // Handle other errors by passing them to the webview in JS + JSONObject data = new JSONObject(); + try { + data.put("errorCode", -6); + data.put("description", "The connection to the server was unsuccessful."); + data.put("url", url); + } catch (JSONException e) { + // Will never happen. + } + pluginManager.postMessage("onReceivedError", data); + } + }; + + // Timeout timer method + final Runnable timeoutCheck = new Runnable() { + public void run() { + try { + synchronized (this) { + wait(loadUrlTimeoutValue); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // If timeout, then stop loading and handle error + if (loadUrlTimeout == currentLoadUrlTimeout) { + cordova.getActivity().runOnUiThread(loadError); + } + } + }; + + final boolean _recreatePlugins = recreatePlugins; + cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + if (loadUrlTimeoutValue > 0) { + cordova.getThreadPool().execute(timeoutCheck); + } + engine.loadUrl(url, _recreatePlugins); + } + }); + } + + + @Override + public void loadUrl(String url) { + loadUrlIntoView(url, true); + } + + @Override + public void showWebPage(String url, boolean openExternal, boolean clearHistory, Map params) { + LOG.d(TAG, "showWebPage(%s, %b, %b, HashMap", url, openExternal, clearHistory); + + // If clearing history + if (clearHistory) { + engine.clearHistory(); + } + + // If loading into our webview + if (!openExternal) { + // Make sure url is in whitelist + if (pluginManager.shouldAllowNavigation(url)) { + // TODO: What about params? + // Load new URL + loadUrlIntoView(url, true); + return; + } + // Load in default viewer if not + LOG.w(TAG, "showWebPage: Cannot load URL into webview since it is not in white list. Loading into browser instead. (URL=" + url + ")"); + } + try { + // Omitting the MIME type for file: URLs causes "No Activity found to handle Intent". + // Adding the MIME type to http: URLs causes them to not be handled by the downloader. + Intent intent = new Intent(Intent.ACTION_VIEW); + Uri uri = Uri.parse(url); + if ("file".equals(uri.getScheme())) { + intent.setDataAndType(uri, resourceApi.getMimeType(uri)); + } else { + intent.setData(uri); + } + cordova.getActivity().startActivity(intent); + } catch (android.content.ActivityNotFoundException e) { + LOG.e(TAG, "Error loading url " + url, e); + } + } + + @Override + @Deprecated + public void showCustomView(View view, WebChromeClient.CustomViewCallback callback) { + // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0 + Log.d(TAG, "showing Custom View"); + // if a view already exists then immediately terminate the new one + if (mCustomView != null) { + callback.onCustomViewHidden(); + return; + } + + // Store the view and its callback for later (to kill it properly) + mCustomView = view; + mCustomViewCallback = callback; + + // Add the custom view to its container. + ViewGroup parent = (ViewGroup) engine.getView().getParent(); + parent.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + Gravity.CENTER)); + + // Hide the content view. + engine.getView().setVisibility(View.GONE); + + // Finally show the custom view container. + parent.setVisibility(View.VISIBLE); + parent.bringToFront(); + } + + @Override + @Deprecated + public void hideCustomView() { + // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0 + Log.d(TAG, "Hiding Custom View"); + if (mCustomView == null) return; + + // Hide the custom view. + mCustomView.setVisibility(View.GONE); + + // Remove the custom view from its container. + ViewGroup parent = (ViewGroup) engine.getView().getParent(); + parent.removeView(mCustomView); + mCustomView = null; + mCustomViewCallback.onCustomViewHidden(); + + // Show the content view. + engine.getView().setVisibility(View.VISIBLE); + } + + @Override + @Deprecated + public boolean isCustomViewShowing() { + return mCustomView != null; + } + + @Override + @Deprecated + public void sendJavascript(String statement) { + nativeToJsMessageQueue.addJavaScript(statement); + } + + @Override + public void sendPluginResult(PluginResult cr, String callbackId) { + nativeToJsMessageQueue.addPluginResult(cr, callbackId); + } + + @Override + public PluginManager getPluginManager() { + return pluginManager; + } + @Override + public CordovaPreferences getPreferences() { + return preferences; + } + @Override + public ICordovaCookieManager getCookieManager() { + return engine.getCookieManager(); + } + @Override + public CordovaResourceApi getResourceApi() { + return resourceApi; + } + @Override + public CordovaWebViewEngine getEngine() { + return engine; + } + @Override + public View getView() { + return engine.getView(); + } + @Override + public Context getContext() { + return engine.getView().getContext(); + } + + private void sendJavascriptEvent(String event) { + if (appPlugin == null) { + appPlugin = (CoreAndroid)pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME); + } + + if (appPlugin == null) { + LOG.w(TAG, "Unable to fire event without existing plugin"); + return; + } + appPlugin.fireJavascriptEvent(event); + } + + @Override + public void setButtonPlumbedToJs(int keyCode, boolean override) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_BACK: + // TODO: Why are search and menu buttons handled separately? + if (override) { + boundKeyCodes.add(keyCode); + } else { + boundKeyCodes.remove(keyCode); + } + return; + default: + throw new IllegalArgumentException("Unsupported keycode: " + keyCode); + } + } + + @Override + public boolean isButtonPlumbedToJs(int keyCode) { + return boundKeyCodes.contains(keyCode); + } + + @Override + public Object postMessage(String id, Object data) { + return pluginManager.postMessage(id, data); + } + + // Engine method proxies: + @Override + public String getUrl() { + return engine.getUrl(); + } + + @Override + public void stopLoading() { + // Clear timeout flag + loadUrlTimeout++; + } + + @Override + public boolean canGoBack() { + return engine.canGoBack(); + } + + @Override + public void clearCache() { + engine.clearCache(); + } + + @Override + @Deprecated + public void clearCache(boolean b) { + engine.clearCache(); + } + + @Override + public void clearHistory() { + engine.clearHistory(); + } + + @Override + public boolean backHistory() { + return engine.goBack(); + } + + /////// LifeCycle methods /////// + @Override + public void onNewIntent(Intent intent) { + if (this.pluginManager != null) { + this.pluginManager.onNewIntent(intent); + } + } + @Override + public void handlePause(boolean keepRunning) { + LOG.d(TAG, "Handle the pause"); + // Send pause event to JavaScript + sendJavascriptEvent("pause"); + + // Forward to plugins + if (pluginManager != null) { + pluginManager.onPause(keepRunning); + } + + // If app doesn't want to run in background + if (!keepRunning) { + // Pause JavaScript timers. This affects all webviews within the app! + engine.setPaused(true); + } + } + @Override + public void handleResume(boolean keepRunning) + { + // Resume JavaScript timers. This affects all webviews within the app! + engine.setPaused(false); + + sendJavascriptEvent("resume"); + + // Forward to plugins + if (this.pluginManager != null) { + this.pluginManager.onResume(keepRunning); + } + } + + @Override + public void handleDestroy() + { + // Cancel pending timeout timer. + loadUrlTimeout++; + + // Forward to plugins + if (this.pluginManager != null) { + this.pluginManager.onDestroy(); + } + + // Load blank page so that JavaScript onunload is called + this.loadUrl("about:blank"); + + // TODO: Should not destroy webview until after about:blank is done loading. + engine.destroy(); + hideCustomView(); + } + + protected class EngineClient implements CordovaWebViewEngine.Client { + private long lastMenuEventTime = 0; + + @Override + public void clearLoadTimeoutTimer() { + loadUrlTimeout++; + } + + @Override + public void onPageStarted(String newUrl) { + LOG.d(TAG, "onPageDidNavigate(" + newUrl + ")"); + boundKeyCodes.clear(); + pluginManager.onReset(); + pluginManager.postMessage("onPageStarted", newUrl); + } + + @Override + public void onReceivedError(int errorCode, String description, String failingUrl) { + clearLoadTimeoutTimer(); + JSONObject data = new JSONObject(); + try { + data.put("errorCode", errorCode); + data.put("description", description); + data.put("url", failingUrl); + } catch (JSONException e) { + e.printStackTrace(); + } + pluginManager.postMessage("onReceivedError", data); + } + + @Override + public void onPageFinishedLoading(String url) { + LOG.d(TAG, "onPageFinished(" + url + ")"); + + clearLoadTimeoutTimer(); + + // Broadcast message that page has loaded + pluginManager.postMessage("onPageFinished", url); + + // Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly + if (engine.getView().getVisibility() != View.VISIBLE) { + Thread t = new Thread(new Runnable() { + public void run() { + try { + Thread.sleep(2000); + cordova.getActivity().runOnUiThread(new Runnable() { + public void run() { + pluginManager.postMessage("spinner", "stop"); + } + }); + } catch (InterruptedException e) { + } + } + }); + t.start(); + } + + // Shutdown if blank loaded + if (url.equals("about:blank")) { + pluginManager.postMessage("exit", null); + } + } + + @Override + public Boolean onKeyDown(int keyCode, KeyEvent event) { + if (boundKeyCodes.contains(keyCode)) + { + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + sendJavascriptEvent("volumedownbutton"); + return true; + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + sendJavascriptEvent("volumeupbutton"); + return true; + } + return null; + } + else if (keyCode == KeyEvent.KEYCODE_BACK) + { + return !engine.canGoBack() || isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK); + } + else if(keyCode == KeyEvent.KEYCODE_MENU) + { + //How did we get here? Is there a childView? + View childView = ((ViewGroup)engine.getView().getParent()).getFocusedChild(); + if(childView != null) + { + //Make sure we close the keyboard if it's present + InputMethodManager imm = (InputMethodManager) cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(childView.getWindowToken(), 0); + cordova.getActivity().openOptionsMenu(); + return true; + } + } + return null; + } + + @Override + public Boolean onKeyUp(int keyCode, KeyEvent event) + { + // If back key + if (keyCode == KeyEvent.KEYCODE_BACK) { + // A custom view is currently displayed (e.g. playing a video) + if(mCustomView != null) { + hideCustomView(); + return true; + } else { + // The webview is currently displayed + // If back key is bound, then send event to JavaScript + if (isButtonPlumbedToJs(KeyEvent.KEYCODE_BACK)) { + sendJavascriptEvent("backbutton"); + return true; + } else { + // If not bound + // Go to previous page in webview if it is possible to go back + if (engine.goBack()) { + return true; + } + // If not, then invoke default behavior + } + } + } + // Legacy + else if (keyCode == KeyEvent.KEYCODE_MENU) { + if (lastMenuEventTime < event.getEventTime()) { + sendJavascriptEvent("menubutton"); + } + lastMenuEventTime = event.getEventTime(); + return null; + } + // If search key + else if (keyCode == KeyEvent.KEYCODE_SEARCH) { + sendJavascriptEvent("searchbutton"); + return true; + } + return null; + } + + @Override + public boolean shouldOverrideUrlLoading(String url) { + // Give plugins the chance to handle the url + if (pluginManager.shouldAllowNavigation(url)) { + // Allow internal navigation + return false; + } else if (pluginManager.shouldOpenExternalUrl(url)) { + // Do nothing other than what the plugins wanted. + // If any returned false, then the request was either blocked + // completely, or handled out-of-band by the plugin. If they all + // returned true, then we should open the URL here. + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + intent.addCategory(Intent.CATEGORY_BROWSABLE); + intent.setComponent(null); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + intent.setSelector(null); + } + getContext().startActivity(intent); + return true; + } catch (android.content.ActivityNotFoundException e) { + Log.e(TAG, "Error loading url " + url, e); + } + return true; + } + // Block by default + return true; + } + } +} diff --git a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java index 6f9db66a..a9a7045e 100755 --- a/framework/src/org/apache/cordova/NativeToJsMessageQueue.java +++ b/framework/src/org/apache/cordova/NativeToJsMessageQueue.java @@ -283,11 +283,11 @@ public class NativeToJsMessageQueue { /** Uses webView.loadUrl("javascript:") to execute messages. */ public static class LoadUrlBridgeMode extends BridgeMode { - private final CordovaWebView webView; + private final CordovaWebViewEngine engine; private final CordovaInterface cordova; - public LoadUrlBridgeMode(CordovaWebView webView, CordovaInterface cordova) { - this.webView = webView; + public LoadUrlBridgeMode(CordovaWebViewEngine engine, CordovaInterface cordova) { + this.engine = engine; this.cordova = cordova; } @@ -297,7 +297,7 @@ public class NativeToJsMessageQueue { public void run() { String js = queue.popAndEncodeAsJs(); if (js != null) { - webView.loadUrl("javascript:" + js); + engine.loadUrl("javascript:" + js, false); } } }); diff --git a/framework/src/org/apache/cordova/PluginManager.java b/framework/src/org/apache/cordova/PluginManager.java index e004b639..9424a6f6 100755 --- a/framework/src/org/apache/cordova/PluginManager.java +++ b/framework/src/org/apache/cordova/PluginManager.java @@ -217,7 +217,7 @@ public class PluginManager { */ public boolean onReceivedHttpAuthRequest(CordovaWebView view, ICordovaHttpAuthHandler handler, String host, String realm) { for (CordovaPlugin plugin : this.pluginMap.values()) { - if (plugin != null && plugin.onReceivedHttpAuthRequest(view, handler, host, realm)) { + if (plugin != null && plugin.onReceivedHttpAuthRequest(app, handler, host, realm)) { return true; } } @@ -236,7 +236,7 @@ public class PluginManager { */ public boolean onReceivedClientCertRequest(CordovaWebView view, ICordovaClientCertRequest request) { for (CordovaPlugin plugin : this.pluginMap.values()) { - if (plugin != null && plugin.onReceivedClientCertRequest(view, request)) { + if (plugin != null && plugin.onReceivedClientCertRequest(app, request)) { return true; } } diff --git a/framework/src/org/apache/cordova/AndroidCookieManager.java b/framework/src/org/apache/cordova/engine/SystemCookieManager.java similarity index 90% rename from framework/src/org/apache/cordova/AndroidCookieManager.java rename to framework/src/org/apache/cordova/engine/SystemCookieManager.java index 16eaa7a8..ae55dfee 100644 --- a/framework/src/org/apache/cordova/AndroidCookieManager.java +++ b/framework/src/org/apache/cordova/engine/SystemCookieManager.java @@ -17,17 +17,20 @@ under the License. */ -package org.apache.cordova; +package org.apache.cordova.engine; import android.os.Build; import android.webkit.CookieManager; import android.webkit.WebView; -class AndroidCookieManager implements ICordovaCookieManager { +import org.apache.cordova.ICordovaCookieManager; + +class SystemCookieManager implements ICordovaCookieManager { + protected final WebView webView; private final CookieManager cookieManager; - public AndroidCookieManager(WebView webview) { + public SystemCookieManager(WebView webview) { webView = webview; cookieManager = CookieManager.getInstance(); diff --git a/framework/src/org/apache/cordova/AndroidExposedJsApi.java b/framework/src/org/apache/cordova/engine/SystemExposedJsApi.java similarity index 89% rename from framework/src/org/apache/cordova/AndroidExposedJsApi.java rename to framework/src/org/apache/cordova/engine/SystemExposedJsApi.java index 5ed4d409..94c3d934 100755 --- a/framework/src/org/apache/cordova/AndroidExposedJsApi.java +++ b/framework/src/org/apache/cordova/engine/SystemExposedJsApi.java @@ -16,9 +16,12 @@ specific language governing permissions and limitations under the License. */ -package org.apache.cordova; +package org.apache.cordova.engine; import android.webkit.JavascriptInterface; + +import org.apache.cordova.CordovaBridge; +import org.apache.cordova.ExposedJsApi; import org.json.JSONException; /** @@ -26,10 +29,10 @@ import org.json.JSONException; * an equivalent entry in CordovaChromeClient.java, and be added to * cordova-js/lib/android/plugin/android/promptbasednativeapi.js */ -class AndroidExposedJsApi implements ExposedJsApi { +class SystemExposedJsApi implements ExposedJsApi { private final CordovaBridge bridge; - AndroidExposedJsApi(CordovaBridge bridge) { + SystemExposedJsApi(CordovaBridge bridge) { this.bridge = bridge; } diff --git a/framework/src/org/apache/cordova/AndroidChromeClient.java b/framework/src/org/apache/cordova/engine/SystemWebChromeClient.java similarity index 88% rename from framework/src/org/apache/cordova/AndroidChromeClient.java rename to framework/src/org/apache/cordova/engine/SystemWebChromeClient.java index 2852a203..7b6c883e 100755 --- a/framework/src/org/apache/cordova/AndroidChromeClient.java +++ b/framework/src/org/apache/cordova/engine/SystemWebChromeClient.java @@ -16,7 +16,7 @@ specific language governing permissions and limitations under the License. */ -package org.apache.cordova; +package org.apache.cordova.engine; import android.annotation.TargetApi; import android.app.Activity; @@ -40,29 +40,34 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; +import org.apache.cordova.CordovaDialogsHelper; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.LOG; + /** * This class is the WebChromeClient that implements callbacks for our web view. * The kind of callbacks that happen here are on the chrome outside the document, * such as onCreateWindow(), onConsoleMessage(), onProgressChanged(), etc. Related * to but different than CordovaWebViewClient. */ -public class AndroidChromeClient extends WebChromeClient { +public class SystemWebChromeClient extends WebChromeClient { - public static final int FILECHOOSER_RESULTCODE = 5173; - private static final String LOG_TAG = "AndroidChromeClient"; + private static final int FILECHOOSER_RESULTCODE = 5173; + private static final String LOG_TAG = "SystemWebChromeClient"; private long MAX_QUOTA = 100 * 1024 * 1024; - protected final CordovaInterface cordova; - protected final AndroidWebView appView; + protected final SystemWebViewEngine parentEngine; // the video progress view private View mVideoProgressView; private CordovaDialogsHelper dialogsHelper; - public AndroidChromeClient(CordovaInterface ctx, AndroidWebView webView) { - this.cordova = ctx; - this.appView = webView; - dialogsHelper = new CordovaDialogsHelper(webView.getContext()); + private WebChromeClient.CustomViewCallback mCustomViewCallback; + private View mCustomView; + + public SystemWebChromeClient(SystemWebViewEngine parentEngine) { + this.parentEngine = parentEngine; + dialogsHelper = new CordovaDialogsHelper(parentEngine.webView.getContext()); } /** @@ -111,7 +116,7 @@ public class AndroidChromeClient extends WebChromeClient { @Override public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) { // Unlike the @JavascriptInterface bridge, this method is always called on the UI thread. - String handledRet = appView.bridge.promptOnJsPrompt(origin, message, defaultValue); + String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue); if (handledRet != null) { result.confirm(handledRet); } else { @@ -178,14 +183,14 @@ public class AndroidChromeClient extends WebChromeClient { // API level 7 is required for this, see if we could lower this using something else @Override public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { - this.appView.showCustomView(view, callback); + parentEngine.getCordovaWebView().showCustomView(view, callback); } @Override public void onHideCustomView() { - this.appView.hideCustomView(); + parentEngine.getCordovaWebView().hideCustomView(); } - + @Override /** * Ask the host application for a custom progress view to show while @@ -198,13 +203,13 @@ public class AndroidChromeClient extends WebChromeClient { // Create a new Loading view programmatically. // create the linear layout - LinearLayout layout = new LinearLayout(this.appView.getContext()); + LinearLayout layout = new LinearLayout(parentEngine.getView().getContext()); layout.setOrientation(LinearLayout.VERTICAL); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); layout.setLayoutParams(layoutParams); // the proress bar - ProgressBar bar = new ProgressBar(this.appView.getContext()); + ProgressBar bar = new ProgressBar(parentEngine.getView().getContext()); LinearLayout.LayoutParams barLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); barLayoutParams.gravity = Gravity.CENTER; bar.setLayoutParams(barLayoutParams); @@ -231,7 +236,7 @@ public class AndroidChromeClient extends WebChromeClient { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); - cordova.startActivityForResult(new CordovaPlugin() { + parentEngine.cordova.startActivityForResult(new CordovaPlugin() { @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { Uri result = intent == null || resultCode != Activity.RESULT_OK ? null : intent.getData(); @@ -246,7 +251,7 @@ public class AndroidChromeClient extends WebChromeClient { public boolean onShowFileChooser(WebView webView, final ValueCallback filePathsCallback, final WebChromeClient.FileChooserParams fileChooserParams) { Intent intent = fileChooserParams.createIntent(); try { - cordova.startActivityForResult(new CordovaPlugin() { + parentEngine.cordova.startActivityForResult(new CordovaPlugin() { @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { Uri[] result = WebChromeClient.FileChooserParams.parseResult(resultCode, intent); @@ -264,5 +269,4 @@ public class AndroidChromeClient extends WebChromeClient { public void destroyLastDialog(){ dialogsHelper.destroyLastDialog(); } - } diff --git a/framework/src/org/apache/cordova/engine/SystemWebView.java b/framework/src/org/apache/cordova/engine/SystemWebView.java new file mode 100644 index 00000000..da07d6a9 --- /dev/null +++ b/framework/src/org/apache/cordova/engine/SystemWebView.java @@ -0,0 +1,88 @@ +package org.apache.cordova.engine; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.CordovaWebViewEngine; +import org.apache.cordova.ScrollEvent; + +/** + * Custom WebView subclass that enables us to capture events needed for Cordova. + */ +public class SystemWebView extends WebView implements CordovaWebViewEngine.EngineView { + private SystemWebViewClient viewClient; + SystemWebChromeClient chromeClient; + private SystemWebViewEngine parentEngine; + private CordovaInterface cordova; + + public SystemWebView(Context context) { + this(context, null); + } + + public SystemWebView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + // Package visibility to enforce that only SystemWebViewEngine should call this method. + void init(SystemWebViewEngine parentEngine, CordovaInterface cordova) { + this.cordova = cordova; + this.parentEngine = parentEngine; + if (this.viewClient == null) { + setWebViewClient(new SystemWebViewClient(parentEngine)); + } + + if (this.chromeClient == null) { + setWebChromeClient(new SystemWebChromeClient(parentEngine)); + } + } + + @Override + public CordovaWebView getCordovaWebView() { + return parentEngine != null ? parentEngine.getCordovaWebView() : null; + } + + @Override + public void setWebViewClient(WebViewClient client) { + viewClient = (SystemWebViewClient)client; + super.setWebViewClient(client); + } + + @Override + public void setWebChromeClient(WebChromeClient client) { + chromeClient = (SystemWebChromeClient)client; + super.setWebChromeClient(client); + } + + @Override + public void onScrollChanged(int l, int t, int oldl, int oldt) + { + super.onScrollChanged(l, t, oldl, oldt); + //We should post a message that the scroll changed + ScrollEvent myEvent = new ScrollEvent(l, t, oldl, oldt, this); + parentEngine.pluginManager.postMessage("onScrollChanged", myEvent); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + Boolean ret = parentEngine.client.onKeyDown(keyCode, event); + if (ret != null) { + return ret.booleanValue(); + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + Boolean ret = parentEngine.client.onKeyUp(keyCode, event); + if (ret != null) { + return ret.booleanValue(); + } + return super.onKeyUp(keyCode, event); + } +} diff --git a/framework/src/org/apache/cordova/AndroidWebViewClient.java b/framework/src/org/apache/cordova/engine/SystemWebViewClient.java similarity index 78% rename from framework/src/org/apache/cordova/AndroidWebViewClient.java rename to framework/src/org/apache/cordova/engine/SystemWebViewClient.java index 06d0c2bd..69dc2f22 100755 --- a/framework/src/org/apache/cordova/AndroidWebViewClient.java +++ b/framework/src/org/apache/cordova/engine/SystemWebViewClient.java @@ -16,14 +16,7 @@ specific language governing permissions and limitations under the License. */ -package org.apache.cordova; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Hashtable; - -import org.json.JSONException; -import org.json.JSONObject; +package org.apache.cordova.engine; import android.annotation.TargetApi; import android.content.pm.ApplicationInfo; @@ -33,7 +26,6 @@ import android.graphics.Bitmap; import android.net.Uri; import android.net.http.SslError; import android.os.Build; -import android.view.View; import android.webkit.ClientCertRequest; import android.webkit.HttpAuthHandler; import android.webkit.SslErrorHandler; @@ -41,6 +33,17 @@ import android.webkit.WebResourceResponse; import android.webkit.WebView; import android.webkit.WebViewClient; +import org.apache.cordova.AuthenticationToken; +import org.apache.cordova.CordovaClientCertRequest; +import org.apache.cordova.CordovaHttpAuthHandler; +import org.apache.cordova.CordovaResourceApi; +import org.apache.cordova.LOG; +import org.apache.cordova.PluginManager; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Hashtable; + /** * This class is the WebViewClient that implements callbacks for our web view. @@ -48,28 +51,19 @@ import android.webkit.WebViewClient; * document instead of the chrome surrounding it, such as onPageStarted(), * shouldOverrideUrlLoading(), etc. Related to but different than * CordovaChromeClient. - * - * @see WebViewClient - * @see WebView guide - * @see CordovaChromeClient - * @see CordovaWebView */ -public class AndroidWebViewClient extends WebViewClient { +public class SystemWebViewClient extends WebViewClient { - private static final String TAG = "AndroidWebViewClient"; - protected final CordovaInterface cordova; - protected final AndroidWebView appView; - protected final CordovaUriHelper helper; + private static final String TAG = "SystemWebViewClient"; + protected final SystemWebViewEngine parentEngine; private boolean doClearHistory = false; boolean isCurrentlyLoading; /** The authorization tokens. */ private Hashtable authenticationTokens = new Hashtable(); - public AndroidWebViewClient(CordovaInterface cordova, AndroidWebView view) { - this.cordova = cordova; - this.appView = view; - helper = new CordovaUriHelper(cordova, view); + public SystemWebViewClient(SystemWebViewEngine parentEngine) { + this.parentEngine = parentEngine; } /** @@ -82,7 +76,7 @@ public class AndroidWebViewClient extends WebViewClient { */ @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - return helper.shouldOverrideUrlLoading(url); + return parentEngine.client.shouldOverrideUrlLoading(url); } /** @@ -100,9 +94,9 @@ public class AndroidWebViewClient extends WebViewClient { } // Check if there is some plugin which can resolve this auth challenge - PluginManager pluginManager = this.appView.pluginManager; - if (pluginManager != null && pluginManager.onReceivedHttpAuthRequest(this.appView, new CordovaHttpAuthHandler(handler), host, realm)) { - this.appView.loadUrlTimeout++; + PluginManager pluginManager = this.parentEngine.pluginManager; + if (pluginManager != null && pluginManager.onReceivedHttpAuthRequest(null, new CordovaHttpAuthHandler(handler), host, realm)) { + parentEngine.client.clearLoadTimeoutTimer(); return; } @@ -123,9 +117,9 @@ public class AndroidWebViewClient extends WebViewClient { { // Check if there is some plugin which can resolve this certificate request - PluginManager pluginManager = this.appView.pluginManager; - if (pluginManager != null && pluginManager.onReceivedClientCertRequest(this.appView, new CordovaClientCertRequest(request))) { - this.appView.loadUrlTimeout++; + PluginManager pluginManager = this.parentEngine.pluginManager; + if (pluginManager != null && pluginManager.onReceivedClientCertRequest(null, new CordovaClientCertRequest(request))) { + parentEngine.client.clearLoadTimeoutTimer(); return; } @@ -146,12 +140,9 @@ public class AndroidWebViewClient extends WebViewClient { public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); isCurrentlyLoading = true; - LOG.d(TAG, "onPageStarted(" + url + ")"); // Flush stale messages & reset plugins. - this.appView.onPageReset(); - - // Broadcast message that page has loaded - this.appView.getPluginManager().postMessage("onPageStarted", url); + parentEngine.bridge.reset(); + parentEngine.client.onPageStarted(url); } /** @@ -170,7 +161,6 @@ public class AndroidWebViewClient extends WebViewClient { return; } isCurrentlyLoading = false; - LOG.d(TAG, "onPageFinished(" + url + ")"); /** * Because of a timing issue we need to clear this history in onPageFinished as well as @@ -182,35 +172,8 @@ public class AndroidWebViewClient extends WebViewClient { view.clearHistory(); this.doClearHistory = false; } + parentEngine.client.onPageFinishedLoading(url); - // Clear timeout flag - appView.loadUrlTimeout++; - - // Broadcast message that page has loaded - this.appView.getPluginManager().postMessage("onPageFinished", url); - - // Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly - if (this.appView.getVisibility() == View.INVISIBLE) { - Thread t = new Thread(new Runnable() { - public void run() { - try { - Thread.sleep(2000); - cordova.getActivity().runOnUiThread(new Runnable() { - public void run() { - appView.getPluginManager().postMessage("spinner", "stop"); - } - }); - } catch (InterruptedException e) { - } - } - }); - t.start(); - } - - // Shutdown if blank loaded - if (url.equals("about:blank")) { - appView.getPluginManager().postMessage("exit", null); - } } /** @@ -230,13 +193,12 @@ public class AndroidWebViewClient extends WebViewClient { } LOG.d(TAG, "CordovaWebViewClient.onReceivedError: Error code=%s Description=%s URL=%s", errorCode, description, failingUrl); - // Clear timeout flag - appView.loadUrlTimeout++; - // If this is a "Protocol Not Supported" error, then revert to the previous // page. If there was no previous page, then punt. The application's config // is likely incorrect (start page set to sms: or something like that) if (errorCode == WebViewClient.ERROR_UNSUPPORTED_SCHEME) { + parentEngine.client.clearLoadTimeoutTimer(); + if (view.canGoBack()) { view.goBack(); return; @@ -244,17 +206,7 @@ public class AndroidWebViewClient extends WebViewClient { super.onReceivedError(view, errorCode, description, failingUrl); } } - - // Handle other errors by passing them to the webview in JS - JSONObject data = new JSONObject(); - try { - data.put("errorCode", errorCode); - data.put("description", description); - data.put("url", failingUrl); - } catch (JSONException e) { - e.printStackTrace(); - } - this.appView.getPluginManager().postMessage("onReceivedError", data); + parentEngine.client.onReceivedError(errorCode, description, failingUrl); } /** @@ -271,8 +223,8 @@ public class AndroidWebViewClient extends WebViewClient { @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - final String packageName = this.cordova.getActivity().getPackageName(); - final PackageManager pm = this.cordova.getActivity().getPackageManager(); + final String packageName = parentEngine.cordova.getActivity().getPackageName(); + final PackageManager pm = parentEngine.cordova.getActivity().getPackageManager(); ApplicationInfo appInfo; try { @@ -370,13 +322,13 @@ public class AndroidWebViewClient extends WebViewClient { try { // Check the against the whitelist and lock out access to the WebView directory // Changing this will cause problems for your application - if (!appView.getPluginManager().shouldAllowRequest(url)) { + if (!parentEngine.pluginManager.shouldAllowRequest(url)) { LOG.w(TAG, "URL blocked by whitelist: " + url); // Results in a 404. return new WebResourceResponse("text/plain", "UTF-8", null); } - CordovaResourceApi resourceApi = appView.getResourceApi(); + CordovaResourceApi resourceApi = parentEngine.resourceApi; Uri origUri = Uri.parse(url); // Allow plugins to intercept WebView requests. Uri remappedUri = resourceApi.remapUri(origUri); @@ -389,7 +341,7 @@ public class AndroidWebViewClient extends WebViewClient { return null; } catch (IOException e) { if (!(e instanceof FileNotFoundException)) { - LOG.e("IceCreamCordovaWebViewClient", "Error occurred while loading a file (returning a 404).", e); + LOG.e(TAG, "Error occurred while loading a file (returning a 404).", e); } // Results in a 404. return new WebResourceResponse("text/plain", "UTF-8", null); diff --git a/framework/src/org/apache/cordova/engine/SystemWebViewEngine.java b/framework/src/org/apache/cordova/engine/SystemWebViewEngine.java new file mode 100755 index 00000000..9cfc99bb --- /dev/null +++ b/framework/src/org/apache/cordova/engine/SystemWebViewEngine.java @@ -0,0 +1,312 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +package org.apache.cordova.engine; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.os.Build; +import android.util.Log; +import android.view.View; +import android.webkit.WebSettings; +import android.webkit.WebSettings.LayoutAlgorithm; +import android.webkit.WebView; + +import org.apache.cordova.CordovaBridge; +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPreferences; +import org.apache.cordova.CordovaResourceApi; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.CordovaWebViewEngine; +import org.apache.cordova.ICordovaCookieManager; +import org.apache.cordova.NativeToJsMessageQueue; +import org.apache.cordova.PluginManager; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + + +/** + * Glue class between CordovaWebView (main Cordova logic) and SystemWebView (the actual View). + * We make the Engine separate from the actual View so that: + * A) We don't need to worry about WebView methods clashing with CordovaWebViewEngine methods + * (e.g.: goBack() is void for WebView, and boolean for CordovaWebViewEngine) + * B) Separating the actual View from the Engine makes API surfaces smaller. + * Class uses two-phase initialization. However, CordovaWebView is responsible for calling .init(). + */ +public class SystemWebViewEngine implements CordovaWebViewEngine { + public static final String TAG = "SystemWebViewEngine"; + + protected final SystemWebView webView; + protected final SystemCookieManager cookieManager; + protected CordovaBridge bridge; + protected CordovaWebViewEngine.Client client; + protected CordovaWebView parentWebView; + protected CordovaInterface cordova; + protected PluginManager pluginManager; + protected CordovaResourceApi resourceApi; + protected NativeToJsMessageQueue nativeToJsMessageQueue; + private BroadcastReceiver receiver; + + /** Used when created via reflection. */ + public SystemWebViewEngine(Context context, CordovaPreferences preferences) { + this(new SystemWebView(context)); + } + + public SystemWebViewEngine(SystemWebView webView) { + this.webView = webView; + cookieManager = new SystemCookieManager(webView); + } + + @Override + public void init(CordovaWebView parentWebView, CordovaInterface cordova, CordovaWebViewEngine.Client client, + CordovaResourceApi resourceApi, PluginManager pluginManager, + NativeToJsMessageQueue nativeToJsMessageQueue) { + if (this.cordova != null) { + throw new IllegalStateException(); + } + this.parentWebView = parentWebView; + this.cordova = cordova; + this.client = client; + this.resourceApi = resourceApi; + this.pluginManager = pluginManager; + this.nativeToJsMessageQueue = nativeToJsMessageQueue; + webView.init(this, cordova); + + initWebViewSettings(); + + nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode.OnlineEventsBridgeModeDelegate() { + @Override + public void setNetworkAvailable(boolean value) { + webView.setNetworkAvailable(value); + } + @Override + public void runOnUiThread(Runnable r) { + SystemWebViewEngine.this.cordova.getActivity().runOnUiThread(r); + } + })); + bridge = new CordovaBridge(pluginManager, nativeToJsMessageQueue); + exposeJsInterface(webView, bridge); + } + + @Override + public CordovaWebView getCordovaWebView() { + return parentWebView; + } + + @Override + public ICordovaCookieManager getCookieManager() { + return cookieManager; + } + + @Override + public View getView() { + return webView; + } + + @SuppressLint("SetJavaScriptEnabled") + @SuppressWarnings("deprecation") + private void initWebViewSettings() { + webView.setInitialScale(0); + webView.setVerticalScrollBarEnabled(false); + // Enable JavaScript + final WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setJavaScriptCanOpenWindowsAutomatically(true); + settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL); + + // Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2) + try { + Method gingerbread_getMethod = WebSettings.class.getMethod("setNavDump", new Class[] { boolean.class }); + + String manufacturer = android.os.Build.MANUFACTURER; + Log.d(TAG, "CordovaWebView is running on device made by: " + manufacturer); + if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB && + android.os.Build.MANUFACTURER.contains("HTC")) + { + gingerbread_getMethod.invoke(settings, true); + } + } catch (NoSuchMethodException e) { + Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8"); + } catch (IllegalArgumentException e) { + Log.d(TAG, "Doing the NavDump failed with bad arguments"); + } catch (IllegalAccessException e) { + Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore"); + } catch (InvocationTargetException e) { + Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore."); + } + + //We don't save any form data in the application + settings.setSaveFormData(false); + settings.setSavePassword(false); + + // Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist + // while we do this + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + settings.setAllowUniversalAccessFromFileURLs(true); + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + settings.setMediaPlaybackRequiresUserGesture(false); + } + // Enable database + // We keep this disabled because we use or shim to get around DOM_EXCEPTION_ERROR_16 + String databasePath = webView.getContext().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath(); + settings.setDatabaseEnabled(true); + settings.setDatabasePath(databasePath); + + + //Determine whether we're in debug or release mode, and turn on Debugging! + ApplicationInfo appInfo = webView.getContext().getApplicationContext().getApplicationInfo(); + if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 && + android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + enableRemoteDebugging(); + } + + settings.setGeolocationDatabasePath(databasePath); + + // Enable DOM storage + settings.setDomStorageEnabled(true); + + // Enable built-in geolocation + settings.setGeolocationEnabled(true); + + // Enable AppCache + // Fix for CB-2282 + settings.setAppCacheMaxSize(5 * 1048576); + settings.setAppCachePath(databasePath); + settings.setAppCacheEnabled(true); + + // Fix for CB-1405 + // Google issue 4641 + settings.getUserAgentString(); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + if (this.receiver == null) { + this.receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + settings.getUserAgentString(); + } + }; + webView.getContext().registerReceiver(this.receiver, intentFilter); + } + // end CB-1405 + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private void enableRemoteDebugging() { + try { + WebView.setWebContentsDebuggingEnabled(true); + } catch (IllegalArgumentException e) { + Log.d(TAG, "You have one job! To turn on Remote Web Debugging! YOU HAVE FAILED! "); + e.printStackTrace(); + } + } + + private static void exposeJsInterface(WebView webView, CordovaBridge bridge) { + if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) { + Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old."); + // Bug being that Java Strings do not get converted to JS strings automatically. + // This isn't hard to work-around on the JS side, but it's easier to just + // use the prompt bridge instead. + return; + } + SystemExposedJsApi exposedJsApi = new SystemExposedJsApi(bridge); + webView.addJavascriptInterface(exposedJsApi, "_cordovaNative"); + } + + + /** + * Load the url into the webview. + */ + @Override + public void loadUrl(final String url, boolean clearNavigationStack) { + webView.loadUrl(url); + } + + @Override + public String getUrl() { + return webView.getUrl(); + } + + @Override + public void stopLoading() { + webView.stopLoading(); + } + + @Override + public void clearCache() { + webView.clearCache(true); + } + + @Override + public void clearHistory() { + webView.clearHistory(); + } + + @Override + public boolean canGoBack() { + return webView.canGoBack(); + } + + /** + * Go to previous page in history. (We manage our own history) + * + * @return true if we went back, false if we are already at top + */ + @Override + public boolean goBack() { + // Check webview first to see if there is a history + // This is needed to support curPage#diffLink, since they are added to parentEngine's history, but not our history url array (JQMobile behavior) + if (webView.canGoBack()) { + webView.goBack(); + return true; + } + return false; + } + + @Override + public void setPaused(boolean value) { + if (value) { + webView.pauseTimers(); + } else { + webView.resumeTimers(); + } + } + + @Override + public void destroy() { + webView.chromeClient.destroyLastDialog(); + webView.destroy(); + // unregister the receiver + if (receiver != null) { + try { + webView.getContext().unregisterReceiver(receiver); + } catch (Exception e) { + Log.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e); + } + } + } +} diff --git a/test/androidTest/src/org/apache/cordova/test/CordovaActivityTest.java b/test/androidTest/src/org/apache/cordova/test/CordovaActivityTest.java index 5fb38479..fc94676b 100644 --- a/test/androidTest/src/org/apache/cordova/test/CordovaActivityTest.java +++ b/test/androidTest/src/org/apache/cordova/test/CordovaActivityTest.java @@ -23,7 +23,8 @@ import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; -import org.apache.cordova.AndroidWebView; +import org.apache.cordova.CordovaWebViewEngine; +import org.apache.cordova.engine.SystemWebView; public class CordovaActivityTest extends BaseCordovaIntegrationTest { private ViewGroup innerContainer; @@ -37,8 +38,9 @@ public class CordovaActivityTest extends BaseCordovaIntegrationTest { } public void testBasicLoad() throws Exception { - assertTrue(testView instanceof AndroidWebView); + assertTrue(testView instanceof SystemWebView); assertTrue(innerContainer instanceof LinearLayout); + assertTrue(((CordovaWebViewEngine.EngineView)testView).getCordovaWebView() != null); String onPageFinishedUrl = testActivity.onPageFinishedUrl.take(); assertEquals(MainTestActivity.START_URL, onPageFinishedUrl); } diff --git a/test/androidTest/src/org/apache/cordova/test/InflateLayoutTest.java b/test/androidTest/src/org/apache/cordova/test/InflateLayoutTest.java index 3899cf1a..6fbc50f8 100644 --- a/test/androidTest/src/org/apache/cordova/test/InflateLayoutTest.java +++ b/test/androidTest/src/org/apache/cordova/test/InflateLayoutTest.java @@ -25,7 +25,7 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; -import org.apache.cordova.AndroidWebView; +import org.apache.cordova.engine.SystemWebView; public class InflateLayoutTest extends ActivityInstrumentationTestCase2 { @@ -48,7 +48,7 @@ public class InflateLayoutTest extends ActivityInstrumentationTestCase2 - diff --git a/test/src/org/apache/cordova/test/CordovaWebViewTestActivity.java b/test/src/org/apache/cordova/test/CordovaWebViewTestActivity.java index 9c63985a..7664d428 100644 --- a/test/src/org/apache/cordova/test/CordovaWebViewTestActivity.java +++ b/test/src/org/apache/cordova/test/CordovaWebViewTestActivity.java @@ -21,15 +21,12 @@ package org.apache.cordova.test; import java.util.concurrent.ArrayBlockingQueue; -import org.apache.cordova.AndroidChromeClient; -import org.apache.cordova.AndroidWebView; -import org.apache.cordova.AndroidWebViewClient; import org.apache.cordova.Config; import org.apache.cordova.CordovaInterfaceImpl; import org.apache.cordova.CordovaWebView; -import org.apache.cordova.CordovaInterface; -import org.apache.cordova.CordovaPlugin; -import org.apache.cordova.test.R; +import org.apache.cordova.CordovaWebViewImpl; +import org.apache.cordova.engine.SystemWebView; +import org.apache.cordova.engine.SystemWebViewEngine; import android.app.Activity; import android.os.Bundle; @@ -61,8 +58,8 @@ public class CordovaWebViewTestActivity extends Activity { //CB-7238: This has to be added now, because it got removed from somewhere else Config.init(this); - AndroidWebView webView = (AndroidWebView) findViewById(R.id.cordovaWebView); - cordovaWebView = webView; + SystemWebView webView = (SystemWebView) findViewById(R.id.cordovaWebView); + cordovaWebView = new CordovaWebViewImpl(this, new SystemWebViewEngine(webView)); cordovaWebView.init(cordovaInterface, Config.getPluginEntries(), Config.getPreferences()); cordovaWebView.loadUrl(START_URL); diff --git a/test/src/org/apache/cordova/test/userwebview.java b/test/src/org/apache/cordova/test/userwebview.java index 6e16c6e7..a37114fa 100755 --- a/test/src/org/apache/cordova/test/userwebview.java +++ b/test/src/org/apache/cordova/test/userwebview.java @@ -23,6 +23,9 @@ import android.webkit.WebView; import android.webkit.GeolocationPermissions.Callback; import org.apache.cordova.*; +import org.apache.cordova.engine.SystemWebChromeClient; +import org.apache.cordova.engine.SystemWebViewClient; +import org.apache.cordova.engine.SystemWebViewEngine; public class userwebview extends MainTestActivity { @@ -32,17 +35,19 @@ public class userwebview extends MainTestActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - testViewClient = new TestViewClient(cordovaInterface, ((AndroidWebView)appView)); - testChromeClient = new TestChromeClient(cordovaInterface, ((AndroidWebView)appView)); + SystemWebViewEngine engine = (SystemWebViewEngine)appView.getEngine(); + testViewClient = new TestViewClient(engine); + testChromeClient = new TestChromeClient(engine); super.init(); - ((AndroidWebView)appView).setWebViewClient(testViewClient); - ((AndroidWebView)appView).setWebChromeClient(testChromeClient); + WebView webView = (WebView)engine.getView(); + webView.setWebViewClient(testViewClient); + webView.setWebChromeClient(testChromeClient); super.loadUrl("file:///android_asset/www/userwebview/index.html"); } - public class TestChromeClient extends AndroidChromeClient { - public TestChromeClient(CordovaInterface ctx, AndroidWebView app) { - super(ctx, app); + public class TestChromeClient extends SystemWebChromeClient { + public TestChromeClient(SystemWebViewEngine parentEngine) { + super(parentEngine); LOG.d("userwebview", "TestChromeClient()"); } @@ -57,9 +62,9 @@ public class userwebview extends MainTestActivity { /** * This class can be used to override the GapViewClient and receive notification of webview events. */ - public class TestViewClient extends AndroidWebViewClient { - public TestViewClient(CordovaInterface ctx, AndroidWebView app) { - super(ctx, app); + public class TestViewClient extends SystemWebViewClient { + public TestViewClient(SystemWebViewEngine parentEngine) { + super(parentEngine); LOG.d("userwebview", "TestViewClient()"); }