123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- /*
- 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.SuppressLint;
- import android.content.Context;
- import android.content.Intent;
- import android.net.Uri;
- import android.view.Gravity;
- import android.view.KeyEvent;
- import android.view.View;
- import android.view.ViewGroup;
- 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";
- private PluginManager pluginManager;
- protected final 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 boolean hasPausedEver;
- // 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<Integer> boundKeyCodes = new HashSet<Integer>();
- public static CordovaWebViewEngine createEngine(Context context, CordovaPreferences preferences) {
- String className = preferences.getString("webview", SystemWebViewEngine.class.getCanonicalName());
- 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(CordovaWebViewEngine cordovaWebViewEngine) {
- this.engine = cordovaWebViewEngine;
- }
- // Convenience method for when creating programmatically (not from Config.xml).
- public void init(CordovaInterface cordova) {
- init(cordova, new ArrayList<PluginEntry>(), new CordovaPreferences());
- }
- @SuppressLint("Assert")
- @Override
- public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences) {
- if (this.cordova != null) {
- throw new IllegalStateException();
- }
- 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) {
- appPlugin = 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<String, Object> 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;
- } else {
- LOG.w(TAG, "showWebPage: Refusing to load URL into webview since it is not in the <allow-navigation> whitelist. URL=" + url);
- return;
- }
- }
- if (!pluginManager.shouldOpenExternalUrl(url)) {
- LOG.w(TAG, "showWebPage: Refusing to send intent for URL since it is not in the <allow-intent> whitelist. URL=" + url);
- return;
- }
- try {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- // To send an intent without CATEGORY_BROWSER, a custom plugin should be used.
- intent.addCategory(Intent.CATEGORY_BROWSABLE);
- Uri uri = Uri.parse(url);
- // 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.
- 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
- if (mCustomView == null) return;
- LOG.d(TAG, "Hiding Custom View");
- // 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:
- case KeyEvent.KEYCODE_MENU:
- // 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) {
- if (!isInitialized()) {
- return;
- }
- hasPausedEver = true;
- pluginManager.onPause(keepRunning);
- sendJavascriptEvent("pause");
- // 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) {
- if (!isInitialized()) {
- return;
- }
- // Resume JavaScript timers. This affects all webviews within the app!
- engine.setPaused(false);
- this.pluginManager.onResume(keepRunning);
- // In order to match the behavior of the other platforms, we only send onResume after an
- // onPause has occurred. The resume event might still be sent if the Activity was killed
- // while waiting for the result of an external Activity once the result is obtained
- if (hasPausedEver) {
- sendJavascriptEvent("resume");
- }
- }
- @Override
- public void handleStart() {
- if (!isInitialized()) {
- return;
- }
- pluginManager.onStart();
- }
- @Override
- public void handleStop() {
- if (!isInitialized()) {
- return;
- }
- pluginManager.onStop();
- }
- @Override
- public void handleDestroy() {
- if (!isInitialized()) {
- return;
- }
- // Cancel pending timeout timer.
- loadUrlTimeout++;
- // Forward to plugins
- this.pluginManager.onDestroy();
- // TODO: about:blank is a bit special (and the default URL for new frames)
- // We should use a blank data: url instead so it's more obvious
- 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 {
- @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 onDispatchKeyEvent(KeyEvent event) {
- int keyCode = event.getKeyCode();
- boolean isBackButton = keyCode == KeyEvent.KEYCODE_BACK;
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- if (isBackButton && mCustomView != null) {
- return true;
- } else if (boundKeyCodes.contains(keyCode)) {
- return true;
- } else if (isBackButton) {
- return engine.canGoBack();
- }
- } else if (event.getAction() == KeyEvent.ACTION_UP) {
- if (isBackButton && mCustomView != null) {
- hideCustomView();
- return true;
- } else if (boundKeyCodes.contains(keyCode)) {
- String eventName = null;
- switch (keyCode) {
- case KeyEvent.KEYCODE_VOLUME_DOWN:
- eventName = "volumedownbutton";
- break;
- case KeyEvent.KEYCODE_VOLUME_UP:
- eventName = "volumeupbutton";
- break;
- case KeyEvent.KEYCODE_SEARCH:
- eventName = "searchbutton";
- break;
- case KeyEvent.KEYCODE_MENU:
- eventName = "menubutton";
- break;
- case KeyEvent.KEYCODE_BACK:
- eventName = "backbutton";
- break;
- }
- if (eventName != null) {
- sendJavascriptEvent(eventName);
- return true;
- }
- } else if (isBackButton) {
- return engine.goBack();
- }
- }
- return null;
- }
- @Override
- public boolean onNavigationAttempt(String url) {
- // Give plugins the chance to handle the url
- if (pluginManager.onOverrideUrlLoading(url)) {
- return true;
- } else if (pluginManager.shouldAllowNavigation(url)) {
- return false;
- } else if (pluginManager.shouldOpenExternalUrl(url)) {
- showWebPage(url, true, false, null);
- return true;
- }
- LOG.w(TAG, "Blocked (possibly sub-frame) navigation to non-allowed URL: " + url);
- return true;
- }
- }
- }
|