CordovaWebViewImpl.java 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. /*
  2. Licensed to the Apache Software Foundation (ASF) under one
  3. or more contributor license agreements. See the NOTICE file
  4. distributed with this work for additional information
  5. regarding copyright ownership. The ASF licenses this file
  6. to you under the Apache License, Version 2.0 (the
  7. "License"); you may not use this file except in compliance
  8. with the License. You may obtain a copy of the License at
  9. http://www.apache.org/licenses/LICENSE-2.0
  10. Unless required by applicable law or agreed to in writing,
  11. software distributed under the License is distributed on an
  12. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  13. KIND, either express or implied. See the License for the
  14. specific language governing permissions and limitations
  15. under the License.
  16. */
  17. package org.apache.cordova;
  18. import android.annotation.SuppressLint;
  19. import android.content.Context;
  20. import android.content.Intent;
  21. import android.net.Uri;
  22. import android.view.Gravity;
  23. import android.view.KeyEvent;
  24. import android.view.View;
  25. import android.view.ViewGroup;
  26. import android.webkit.WebChromeClient;
  27. import android.widget.FrameLayout;
  28. import org.apache.cordova.engine.SystemWebViewEngine;
  29. import org.json.JSONException;
  30. import org.json.JSONObject;
  31. import java.lang.reflect.Constructor;
  32. import java.util.ArrayList;
  33. import java.util.HashSet;
  34. import java.util.List;
  35. import java.util.Map;
  36. import java.util.Set;
  37. /**
  38. * Main class for interacting with a Cordova webview. Manages plugins, events, and a CordovaWebViewEngine.
  39. * Class uses two-phase initialization. You must call init() before calling any other methods.
  40. */
  41. public class CordovaWebViewImpl implements CordovaWebView {
  42. public static final String TAG = "CordovaWebViewImpl";
  43. private PluginManager pluginManager;
  44. protected final CordovaWebViewEngine engine;
  45. private CordovaInterface cordova;
  46. // Flag to track that a loadUrl timeout occurred
  47. private int loadUrlTimeout = 0;
  48. private CordovaResourceApi resourceApi;
  49. private CordovaPreferences preferences;
  50. private CoreAndroid appPlugin;
  51. private NativeToJsMessageQueue nativeToJsMessageQueue;
  52. private EngineClient engineClient = new EngineClient();
  53. private boolean hasPausedEver;
  54. // The URL passed to loadUrl(), not necessarily the URL of the current page.
  55. String loadedUrl;
  56. /** custom view created by the browser (a video player for example) */
  57. private View mCustomView;
  58. private WebChromeClient.CustomViewCallback mCustomViewCallback;
  59. private Set<Integer> boundKeyCodes = new HashSet<Integer>();
  60. public static CordovaWebViewEngine createEngine(Context context, CordovaPreferences preferences) {
  61. String className = preferences.getString("webview", SystemWebViewEngine.class.getCanonicalName());
  62. try {
  63. Class<?> webViewClass = Class.forName(className);
  64. Constructor<?> constructor = webViewClass.getConstructor(Context.class, CordovaPreferences.class);
  65. return (CordovaWebViewEngine) constructor.newInstance(context, preferences);
  66. } catch (Exception e) {
  67. throw new RuntimeException("Failed to create webview. ", e);
  68. }
  69. }
  70. public CordovaWebViewImpl(CordovaWebViewEngine cordovaWebViewEngine) {
  71. this.engine = cordovaWebViewEngine;
  72. }
  73. // Convenience method for when creating programmatically (not from Config.xml).
  74. public void init(CordovaInterface cordova) {
  75. init(cordova, new ArrayList<PluginEntry>(), new CordovaPreferences());
  76. }
  77. @SuppressLint("Assert")
  78. @Override
  79. public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences) {
  80. if (this.cordova != null) {
  81. throw new IllegalStateException();
  82. }
  83. this.cordova = cordova;
  84. this.preferences = preferences;
  85. pluginManager = new PluginManager(this, this.cordova, pluginEntries);
  86. resourceApi = new CordovaResourceApi(engine.getView().getContext(), pluginManager);
  87. nativeToJsMessageQueue = new NativeToJsMessageQueue();
  88. nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.NoOpBridgeMode());
  89. nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.LoadUrlBridgeMode(engine, cordova));
  90. if (preferences.getBoolean("DisallowOverscroll", false)) {
  91. engine.getView().setOverScrollMode(View.OVER_SCROLL_NEVER);
  92. }
  93. engine.init(this, cordova, engineClient, resourceApi, pluginManager, nativeToJsMessageQueue);
  94. // This isn't enforced by the compiler, so assert here.
  95. assert engine.getView() instanceof CordovaWebViewEngine.EngineView;
  96. pluginManager.addService(CoreAndroid.PLUGIN_NAME, "org.apache.cordova.CoreAndroid");
  97. pluginManager.init();
  98. }
  99. @Override
  100. public boolean isInitialized() {
  101. return cordova != null;
  102. }
  103. @Override
  104. public void loadUrlIntoView(final String url, boolean recreatePlugins) {
  105. LOG.d(TAG, ">>> loadUrl(" + url + ")");
  106. if (url.equals("about:blank") || url.startsWith("javascript:")) {
  107. engine.loadUrl(url, false);
  108. return;
  109. }
  110. recreatePlugins = recreatePlugins || (loadedUrl == null);
  111. if (recreatePlugins) {
  112. // Don't re-initialize on first load.
  113. if (loadedUrl != null) {
  114. appPlugin = null;
  115. pluginManager.init();
  116. }
  117. loadedUrl = url;
  118. }
  119. // Create a timeout timer for loadUrl
  120. final int currentLoadUrlTimeout = loadUrlTimeout;
  121. final int loadUrlTimeoutValue = preferences.getInteger("LoadUrlTimeoutValue", 20000);
  122. // Timeout error method
  123. final Runnable loadError = new Runnable() {
  124. public void run() {
  125. stopLoading();
  126. LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!");
  127. // Handle other errors by passing them to the webview in JS
  128. JSONObject data = new JSONObject();
  129. try {
  130. data.put("errorCode", -6);
  131. data.put("description", "The connection to the server was unsuccessful.");
  132. data.put("url", url);
  133. } catch (JSONException e) {
  134. // Will never happen.
  135. }
  136. pluginManager.postMessage("onReceivedError", data);
  137. }
  138. };
  139. // Timeout timer method
  140. final Runnable timeoutCheck = new Runnable() {
  141. public void run() {
  142. try {
  143. synchronized (this) {
  144. wait(loadUrlTimeoutValue);
  145. }
  146. } catch (InterruptedException e) {
  147. e.printStackTrace();
  148. }
  149. // If timeout, then stop loading and handle error
  150. if (loadUrlTimeout == currentLoadUrlTimeout) {
  151. cordova.getActivity().runOnUiThread(loadError);
  152. }
  153. }
  154. };
  155. final boolean _recreatePlugins = recreatePlugins;
  156. cordova.getActivity().runOnUiThread(new Runnable() {
  157. public void run() {
  158. if (loadUrlTimeoutValue > 0) {
  159. cordova.getThreadPool().execute(timeoutCheck);
  160. }
  161. engine.loadUrl(url, _recreatePlugins);
  162. }
  163. });
  164. }
  165. @Override
  166. public void loadUrl(String url) {
  167. loadUrlIntoView(url, true);
  168. }
  169. @Override
  170. public void showWebPage(String url, boolean openExternal, boolean clearHistory, Map<String, Object> params) {
  171. LOG.d(TAG, "showWebPage(%s, %b, %b, HashMap)", url, openExternal, clearHistory);
  172. // If clearing history
  173. if (clearHistory) {
  174. engine.clearHistory();
  175. }
  176. // If loading into our webview
  177. if (!openExternal) {
  178. // Make sure url is in whitelist
  179. if (pluginManager.shouldAllowNavigation(url)) {
  180. // TODO: What about params?
  181. // Load new URL
  182. loadUrlIntoView(url, true);
  183. return;
  184. } else {
  185. LOG.w(TAG, "showWebPage: Refusing to load URL into webview since it is not in the <allow-navigation> whitelist. URL=" + url);
  186. return;
  187. }
  188. }
  189. if (!pluginManager.shouldOpenExternalUrl(url)) {
  190. LOG.w(TAG, "showWebPage: Refusing to send intent for URL since it is not in the <allow-intent> whitelist. URL=" + url);
  191. return;
  192. }
  193. try {
  194. Intent intent = new Intent(Intent.ACTION_VIEW);
  195. // To send an intent without CATEGORY_BROWSER, a custom plugin should be used.
  196. intent.addCategory(Intent.CATEGORY_BROWSABLE);
  197. Uri uri = Uri.parse(url);
  198. // Omitting the MIME type for file: URLs causes "No Activity found to handle Intent".
  199. // Adding the MIME type to http: URLs causes them to not be handled by the downloader.
  200. if ("file".equals(uri.getScheme())) {
  201. intent.setDataAndType(uri, resourceApi.getMimeType(uri));
  202. } else {
  203. intent.setData(uri);
  204. }
  205. cordova.getActivity().startActivity(intent);
  206. } catch (android.content.ActivityNotFoundException e) {
  207. LOG.e(TAG, "Error loading url " + url, e);
  208. }
  209. }
  210. @Override
  211. @Deprecated
  212. public void showCustomView(View view, WebChromeClient.CustomViewCallback callback) {
  213. // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
  214. LOG.d(TAG, "showing Custom View");
  215. // if a view already exists then immediately terminate the new one
  216. if (mCustomView != null) {
  217. callback.onCustomViewHidden();
  218. return;
  219. }
  220. // Store the view and its callback for later (to kill it properly)
  221. mCustomView = view;
  222. mCustomViewCallback = callback;
  223. // Add the custom view to its container.
  224. ViewGroup parent = (ViewGroup) engine.getView().getParent();
  225. parent.addView(view, new FrameLayout.LayoutParams(
  226. ViewGroup.LayoutParams.MATCH_PARENT,
  227. ViewGroup.LayoutParams.MATCH_PARENT,
  228. Gravity.CENTER));
  229. // Hide the content view.
  230. engine.getView().setVisibility(View.GONE);
  231. // Finally show the custom view container.
  232. parent.setVisibility(View.VISIBLE);
  233. parent.bringToFront();
  234. }
  235. @Override
  236. @Deprecated
  237. public void hideCustomView() {
  238. // This code is adapted from the original Android Browser code, licensed under the Apache License, Version 2.0
  239. if (mCustomView == null) return;
  240. LOG.d(TAG, "Hiding Custom View");
  241. // Hide the custom view.
  242. mCustomView.setVisibility(View.GONE);
  243. // Remove the custom view from its container.
  244. ViewGroup parent = (ViewGroup) engine.getView().getParent();
  245. parent.removeView(mCustomView);
  246. mCustomView = null;
  247. mCustomViewCallback.onCustomViewHidden();
  248. // Show the content view.
  249. engine.getView().setVisibility(View.VISIBLE);
  250. }
  251. @Override
  252. @Deprecated
  253. public boolean isCustomViewShowing() {
  254. return mCustomView != null;
  255. }
  256. @Override
  257. @Deprecated
  258. public void sendJavascript(String statement) {
  259. nativeToJsMessageQueue.addJavaScript(statement);
  260. }
  261. @Override
  262. public void sendPluginResult(PluginResult cr, String callbackId) {
  263. nativeToJsMessageQueue.addPluginResult(cr, callbackId);
  264. }
  265. @Override
  266. public PluginManager getPluginManager() {
  267. return pluginManager;
  268. }
  269. @Override
  270. public CordovaPreferences getPreferences() {
  271. return preferences;
  272. }
  273. @Override
  274. public ICordovaCookieManager getCookieManager() {
  275. return engine.getCookieManager();
  276. }
  277. @Override
  278. public CordovaResourceApi getResourceApi() {
  279. return resourceApi;
  280. }
  281. @Override
  282. public CordovaWebViewEngine getEngine() {
  283. return engine;
  284. }
  285. @Override
  286. public View getView() {
  287. return engine.getView();
  288. }
  289. @Override
  290. public Context getContext() {
  291. return engine.getView().getContext();
  292. }
  293. private void sendJavascriptEvent(String event) {
  294. if (appPlugin == null) {
  295. appPlugin = (CoreAndroid)pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME);
  296. }
  297. if (appPlugin == null) {
  298. LOG.w(TAG, "Unable to fire event without existing plugin");
  299. return;
  300. }
  301. appPlugin.fireJavascriptEvent(event);
  302. }
  303. @Override
  304. public void setButtonPlumbedToJs(int keyCode, boolean override) {
  305. switch (keyCode) {
  306. case KeyEvent.KEYCODE_VOLUME_DOWN:
  307. case KeyEvent.KEYCODE_VOLUME_UP:
  308. case KeyEvent.KEYCODE_BACK:
  309. case KeyEvent.KEYCODE_MENU:
  310. // TODO: Why are search and menu buttons handled separately?
  311. if (override) {
  312. boundKeyCodes.add(keyCode);
  313. } else {
  314. boundKeyCodes.remove(keyCode);
  315. }
  316. return;
  317. default:
  318. throw new IllegalArgumentException("Unsupported keycode: " + keyCode);
  319. }
  320. }
  321. @Override
  322. public boolean isButtonPlumbedToJs(int keyCode) {
  323. return boundKeyCodes.contains(keyCode);
  324. }
  325. @Override
  326. public Object postMessage(String id, Object data) {
  327. return pluginManager.postMessage(id, data);
  328. }
  329. // Engine method proxies:
  330. @Override
  331. public String getUrl() {
  332. return engine.getUrl();
  333. }
  334. @Override
  335. public void stopLoading() {
  336. // Clear timeout flag
  337. loadUrlTimeout++;
  338. }
  339. @Override
  340. public boolean canGoBack() {
  341. return engine.canGoBack();
  342. }
  343. @Override
  344. public void clearCache() {
  345. engine.clearCache();
  346. }
  347. @Override
  348. @Deprecated
  349. public void clearCache(boolean b) {
  350. engine.clearCache();
  351. }
  352. @Override
  353. public void clearHistory() {
  354. engine.clearHistory();
  355. }
  356. @Override
  357. public boolean backHistory() {
  358. return engine.goBack();
  359. }
  360. /////// LifeCycle methods ///////
  361. @Override
  362. public void onNewIntent(Intent intent) {
  363. if (this.pluginManager != null) {
  364. this.pluginManager.onNewIntent(intent);
  365. }
  366. }
  367. @Override
  368. public void handlePause(boolean keepRunning) {
  369. if (!isInitialized()) {
  370. return;
  371. }
  372. hasPausedEver = true;
  373. pluginManager.onPause(keepRunning);
  374. sendJavascriptEvent("pause");
  375. // If app doesn't want to run in background
  376. if (!keepRunning) {
  377. // Pause JavaScript timers. This affects all webviews within the app!
  378. engine.setPaused(true);
  379. }
  380. }
  381. @Override
  382. public void handleResume(boolean keepRunning) {
  383. if (!isInitialized()) {
  384. return;
  385. }
  386. // Resume JavaScript timers. This affects all webviews within the app!
  387. engine.setPaused(false);
  388. this.pluginManager.onResume(keepRunning);
  389. // In order to match the behavior of the other platforms, we only send onResume after an
  390. // onPause has occurred. The resume event might still be sent if the Activity was killed
  391. // while waiting for the result of an external Activity once the result is obtained
  392. if (hasPausedEver) {
  393. sendJavascriptEvent("resume");
  394. }
  395. }
  396. @Override
  397. public void handleStart() {
  398. if (!isInitialized()) {
  399. return;
  400. }
  401. pluginManager.onStart();
  402. }
  403. @Override
  404. public void handleStop() {
  405. if (!isInitialized()) {
  406. return;
  407. }
  408. pluginManager.onStop();
  409. }
  410. @Override
  411. public void handleDestroy() {
  412. if (!isInitialized()) {
  413. return;
  414. }
  415. // Cancel pending timeout timer.
  416. loadUrlTimeout++;
  417. // Forward to plugins
  418. this.pluginManager.onDestroy();
  419. // TODO: about:blank is a bit special (and the default URL for new frames)
  420. // We should use a blank data: url instead so it's more obvious
  421. this.loadUrl("about:blank");
  422. // TODO: Should not destroy webview until after about:blank is done loading.
  423. engine.destroy();
  424. hideCustomView();
  425. }
  426. protected class EngineClient implements CordovaWebViewEngine.Client {
  427. @Override
  428. public void clearLoadTimeoutTimer() {
  429. loadUrlTimeout++;
  430. }
  431. @Override
  432. public void onPageStarted(String newUrl) {
  433. LOG.d(TAG, "onPageDidNavigate(" + newUrl + ")");
  434. boundKeyCodes.clear();
  435. pluginManager.onReset();
  436. pluginManager.postMessage("onPageStarted", newUrl);
  437. }
  438. @Override
  439. public void onReceivedError(int errorCode, String description, String failingUrl) {
  440. clearLoadTimeoutTimer();
  441. JSONObject data = new JSONObject();
  442. try {
  443. data.put("errorCode", errorCode);
  444. data.put("description", description);
  445. data.put("url", failingUrl);
  446. } catch (JSONException e) {
  447. e.printStackTrace();
  448. }
  449. pluginManager.postMessage("onReceivedError", data);
  450. }
  451. @Override
  452. public void onPageFinishedLoading(String url) {
  453. LOG.d(TAG, "onPageFinished(" + url + ")");
  454. clearLoadTimeoutTimer();
  455. // Broadcast message that page has loaded
  456. pluginManager.postMessage("onPageFinished", url);
  457. // Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly
  458. if (engine.getView().getVisibility() != View.VISIBLE) {
  459. Thread t = new Thread(new Runnable() {
  460. public void run() {
  461. try {
  462. Thread.sleep(2000);
  463. cordova.getActivity().runOnUiThread(new Runnable() {
  464. public void run() {
  465. pluginManager.postMessage("spinner", "stop");
  466. }
  467. });
  468. } catch (InterruptedException e) {
  469. }
  470. }
  471. });
  472. t.start();
  473. }
  474. // Shutdown if blank loaded
  475. if (url.equals("about:blank")) {
  476. pluginManager.postMessage("exit", null);
  477. }
  478. }
  479. @Override
  480. public Boolean onDispatchKeyEvent(KeyEvent event) {
  481. int keyCode = event.getKeyCode();
  482. boolean isBackButton = keyCode == KeyEvent.KEYCODE_BACK;
  483. if (event.getAction() == KeyEvent.ACTION_DOWN) {
  484. if (isBackButton && mCustomView != null) {
  485. return true;
  486. } else if (boundKeyCodes.contains(keyCode)) {
  487. return true;
  488. } else if (isBackButton) {
  489. return engine.canGoBack();
  490. }
  491. } else if (event.getAction() == KeyEvent.ACTION_UP) {
  492. if (isBackButton && mCustomView != null) {
  493. hideCustomView();
  494. return true;
  495. } else if (boundKeyCodes.contains(keyCode)) {
  496. String eventName = null;
  497. switch (keyCode) {
  498. case KeyEvent.KEYCODE_VOLUME_DOWN:
  499. eventName = "volumedownbutton";
  500. break;
  501. case KeyEvent.KEYCODE_VOLUME_UP:
  502. eventName = "volumeupbutton";
  503. break;
  504. case KeyEvent.KEYCODE_SEARCH:
  505. eventName = "searchbutton";
  506. break;
  507. case KeyEvent.KEYCODE_MENU:
  508. eventName = "menubutton";
  509. break;
  510. case KeyEvent.KEYCODE_BACK:
  511. eventName = "backbutton";
  512. break;
  513. }
  514. if (eventName != null) {
  515. sendJavascriptEvent(eventName);
  516. return true;
  517. }
  518. } else if (isBackButton) {
  519. return engine.goBack();
  520. }
  521. }
  522. return null;
  523. }
  524. @Override
  525. public boolean onNavigationAttempt(String url) {
  526. // Give plugins the chance to handle the url
  527. if (pluginManager.onOverrideUrlLoading(url)) {
  528. return true;
  529. } else if (pluginManager.shouldAllowNavigation(url)) {
  530. return false;
  531. } else if (pluginManager.shouldOpenExternalUrl(url)) {
  532. showWebPage(url, true, false, null);
  533. return true;
  534. }
  535. LOG.w(TAG, "Blocked (possibly sub-frame) navigation to non-allowed URL: " + url);
  536. return true;
  537. }
  538. }
  539. }