react-native-webview.git

RNCWebViewManager.java 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. package com.reactnativecommunity.webview;
  2. import android.annotation.SuppressLint;
  3. import android.annotation.TargetApi;
  4. import android.app.DownloadManager;
  5. import android.content.Context;
  6. import android.content.Intent;
  7. import android.graphics.Bitmap;
  8. import android.net.Uri;
  9. import android.os.Build;
  10. import android.os.Environment;
  11. import android.text.TextUtils;
  12. import android.view.View;
  13. import android.view.ViewGroup.LayoutParams;
  14. import android.webkit.ConsoleMessage;
  15. import android.webkit.CookieManager;
  16. import android.webkit.DownloadListener;
  17. import android.webkit.GeolocationPermissions;
  18. import android.webkit.JavascriptInterface;
  19. import android.webkit.URLUtil;
  20. import android.webkit.ValueCallback;
  21. import android.webkit.WebChromeClient;
  22. import android.webkit.WebResourceRequest;
  23. import android.webkit.WebSettings;
  24. import android.webkit.WebView;
  25. import android.webkit.WebViewClient;
  26. import com.facebook.react.bridge.Arguments;
  27. import com.facebook.react.bridge.LifecycleEventListener;
  28. import com.facebook.react.bridge.ReactContext;
  29. import com.facebook.react.bridge.ReadableArray;
  30. import com.facebook.react.bridge.ReadableMap;
  31. import com.facebook.react.bridge.ReadableMapKeySetIterator;
  32. import com.facebook.react.bridge.WritableMap;
  33. import com.facebook.react.common.MapBuilder;
  34. import com.facebook.react.common.build.ReactBuildConfig;
  35. import com.facebook.react.module.annotations.ReactModule;
  36. import com.facebook.react.uimanager.SimpleViewManager;
  37. import com.facebook.react.uimanager.ThemedReactContext;
  38. import com.facebook.react.uimanager.UIManagerModule;
  39. import com.facebook.react.uimanager.annotations.ReactProp;
  40. import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
  41. import com.facebook.react.uimanager.events.Event;
  42. import com.facebook.react.uimanager.events.EventDispatcher;
  43. import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
  44. import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
  45. import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
  46. import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
  47. import com.reactnativecommunity.webview.events.TopMessageEvent;
  48. import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
  49. import org.json.JSONException;
  50. import org.json.JSONObject;
  51. import java.io.UnsupportedEncodingException;
  52. import java.net.MalformedURLException;
  53. import java.net.URL;
  54. import java.net.URLEncoder;
  55. import java.util.HashMap;
  56. import java.util.Locale;
  57. import java.util.Map;
  58. import javax.annotation.Nullable;
  59. /**
  60. * Manages instances of {@link WebView}
  61. * <p>
  62. * Can accept following commands:
  63. * - GO_BACK
  64. * - GO_FORWARD
  65. * - RELOAD
  66. * - LOAD_URL
  67. * <p>
  68. * {@link WebView} instances could emit following direct events:
  69. * - topLoadingFinish
  70. * - topLoadingStart
  71. * - topLoadingStart
  72. * - topLoadingProgress
  73. * - topShouldStartLoadWithRequest
  74. * <p>
  75. * Each event will carry the following properties:
  76. * - target - view's react tag
  77. * - url - url set for the webview
  78. * - loading - whether webview is in a loading state
  79. * - title - title of the current page
  80. * - canGoBack - boolean, whether there is anything on a history stack to go back
  81. * - canGoForward - boolean, whether it is possible to request GO_FORWARD command
  82. */
  83. @ReactModule(name = RNCWebViewManager.REACT_CLASS)
  84. public class RNCWebViewManager extends SimpleViewManager<WebView> {
  85. public static final int COMMAND_GO_BACK = 1;
  86. public static final int COMMAND_GO_FORWARD = 2;
  87. public static final int COMMAND_RELOAD = 3;
  88. public static final int COMMAND_STOP_LOADING = 4;
  89. public static final int COMMAND_POST_MESSAGE = 5;
  90. public static final int COMMAND_INJECT_JAVASCRIPT = 6;
  91. public static final int COMMAND_LOAD_URL = 7;
  92. protected static final String REACT_CLASS = "RNCWebView";
  93. protected static final String HTML_ENCODING = "UTF-8";
  94. protected static final String HTML_MIME_TYPE = "text/html";
  95. protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
  96. protected static final String HTTP_METHOD_POST = "POST";
  97. // Use `webView.loadUrl("about:blank")` to reliably reset the view
  98. // state and release page resources (including any running JavaScript).
  99. protected static final String BLANK_URL = "about:blank";
  100. protected WebViewConfig mWebViewConfig;
  101. public RNCWebViewManager() {
  102. mWebViewConfig = new WebViewConfig() {
  103. public void configWebView(WebView webView) {
  104. }
  105. };
  106. }
  107. public RNCWebViewManager(WebViewConfig webViewConfig) {
  108. mWebViewConfig = webViewConfig;
  109. }
  110. protected static void dispatchEvent(WebView webView, Event event) {
  111. ReactContext reactContext = (ReactContext) webView.getContext();
  112. EventDispatcher eventDispatcher =
  113. reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
  114. eventDispatcher.dispatchEvent(event);
  115. }
  116. @Override
  117. public String getName() {
  118. return REACT_CLASS;
  119. }
  120. protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) {
  121. return new RNCWebView(reactContext);
  122. }
  123. @Override
  124. @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  125. protected WebView createViewInstance(ThemedReactContext reactContext) {
  126. RNCWebView webView = createRNCWebViewInstance(reactContext);
  127. webView.setWebChromeClient(new WebChromeClient() {
  128. @Override
  129. public boolean onConsoleMessage(ConsoleMessage message) {
  130. if (ReactBuildConfig.DEBUG) {
  131. return super.onConsoleMessage(message);
  132. }
  133. // Ignore console logs in non debug builds.
  134. return true;
  135. }
  136. @Override
  137. public void onProgressChanged(WebView webView, int newProgress) {
  138. super.onProgressChanged(webView, newProgress);
  139. WritableMap event = Arguments.createMap();
  140. event.putDouble("target", webView.getId());
  141. event.putString("title", webView.getTitle());
  142. event.putBoolean("canGoBack", webView.canGoBack());
  143. event.putBoolean("canGoForward", webView.canGoForward());
  144. event.putDouble("progress", (float) newProgress / 100);
  145. dispatchEvent(
  146. webView,
  147. new TopLoadingProgressEvent(
  148. webView.getId(),
  149. event));
  150. }
  151. @Override
  152. public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
  153. callback.invoke(origin, true, false);
  154. }
  155. protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
  156. getModule(reactContext).startPhotoPickerIntent(filePathCallback, acceptType);
  157. }
  158. protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
  159. getModule(reactContext).startPhotoPickerIntent(filePathCallback, "");
  160. }
  161. protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
  162. getModule(reactContext).startPhotoPickerIntent(filePathCallback, acceptType);
  163. }
  164. @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  165. @Override
  166. public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
  167. String[] acceptTypes = fileChooserParams.getAcceptTypes();
  168. boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
  169. Intent intent = fileChooserParams.createIntent();
  170. return getModule(reactContext).startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
  171. }
  172. });
  173. reactContext.addLifecycleEventListener(webView);
  174. mWebViewConfig.configWebView(webView);
  175. WebSettings settings = webView.getSettings();
  176. settings.setBuiltInZoomControls(true);
  177. settings.setDisplayZoomControls(false);
  178. settings.setDomStorageEnabled(true);
  179. settings.setAllowFileAccess(false);
  180. settings.setAllowContentAccess(false);
  181. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
  182. settings.setAllowFileAccessFromFileURLs(false);
  183. setAllowUniversalAccessFromFileURLs(webView, false);
  184. }
  185. setMixedContentMode(webView, "never");
  186. // Fixes broken full-screen modals/galleries due to body height being 0.
  187. webView.setLayoutParams(
  188. new LayoutParams(LayoutParams.MATCH_PARENT,
  189. LayoutParams.MATCH_PARENT));
  190. setGeolocationEnabled(webView, false);
  191. if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
  192. WebView.setWebContentsDebuggingEnabled(true);
  193. }
  194. webView.setDownloadListener(new DownloadListener() {
  195. public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
  196. RNCWebViewModule module = getModule(reactContext);
  197. DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
  198. String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype);
  199. String downloadMessage = "Downloading " + fileName;
  200. //Attempt to add cookie, if it exists
  201. URL urlObj = null;
  202. try {
  203. urlObj = new URL(url);
  204. String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost();
  205. String cookie = CookieManager.getInstance().getCookie(baseUrl);
  206. request.addRequestHeader("Cookie", cookie);
  207. System.out.println("Got cookie for DownloadManager: " + cookie);
  208. } catch (MalformedURLException e) {
  209. System.out.println("Error getting cookie for DownloadManager: " + e.toString());
  210. e.printStackTrace();
  211. }
  212. //Finish setting up request
  213. request.addRequestHeader("User-Agent", userAgent);
  214. request.setTitle(fileName);
  215. request.setDescription(downloadMessage);
  216. request.allowScanningByMediaScanner();
  217. request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
  218. request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
  219. module.setDownloadRequest(request);
  220. if (module.grantFileDownloaderPermissions()) {
  221. module.downloadFile();
  222. }
  223. }
  224. });
  225. return webView;
  226. }
  227. @ReactProp(name = "javaScriptEnabled")
  228. public void setJavaScriptEnabled(WebView view, boolean enabled) {
  229. view.getSettings().setJavaScriptEnabled(enabled);
  230. }
  231. @ReactProp(name = "showsHorizontalScrollIndicator")
  232. public void setShowsHorizontalScrollIndicator(WebView view, boolean enabled) {
  233. view.setHorizontalScrollBarEnabled(enabled);
  234. }
  235. @ReactProp(name = "showsVerticalScrollIndicator")
  236. public void setShowsVerticalScrollIndicator(WebView view, boolean enabled) {
  237. view.setVerticalScrollBarEnabled(enabled);
  238. }
  239. @ReactProp(name = "cacheEnabled")
  240. public void setCacheEnabled(WebView view, boolean enabled) {
  241. if (enabled) {
  242. Context ctx = view.getContext();
  243. if (ctx != null) {
  244. view.getSettings().setAppCachePath(ctx.getCacheDir().getAbsolutePath());
  245. view.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
  246. view.getSettings().setAppCacheEnabled(true);
  247. }
  248. } else {
  249. view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
  250. view.getSettings().setAppCacheEnabled(false);
  251. }
  252. }
  253. @ReactProp(name = "androidHardwareAccelerationDisabled")
  254. public void setHardwareAccelerationDisabled(WebView view, boolean disabled) {
  255. if (disabled) {
  256. view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
  257. } else {
  258. view.setLayerType(View.LAYER_TYPE_NONE, null);
  259. }
  260. }
  261. @ReactProp(name = "overScrollMode")
  262. public void setOverScrollMode(WebView view, String overScrollModeString) {
  263. Integer overScrollMode;
  264. switch (overScrollModeString) {
  265. case "never":
  266. overScrollMode = View.OVER_SCROLL_NEVER;
  267. break;
  268. case "content":
  269. overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS;
  270. break;
  271. case "always":
  272. default:
  273. overScrollMode = View.OVER_SCROLL_ALWAYS;
  274. break;
  275. }
  276. view.setOverScrollMode(overScrollMode);
  277. }
  278. @ReactProp(name = "thirdPartyCookiesEnabled")
  279. public void setThirdPartyCookiesEnabled(WebView view, boolean enabled) {
  280. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  281. CookieManager.getInstance().setAcceptThirdPartyCookies(view, enabled);
  282. }
  283. }
  284. @ReactProp(name = "textZoom")
  285. public void setTextZoom(WebView view, int value) {
  286. view.getSettings().setTextZoom(value);
  287. }
  288. @ReactProp(name = "scalesPageToFit")
  289. public void setScalesPageToFit(WebView view, boolean enabled) {
  290. view.getSettings().setLoadWithOverviewMode(enabled);
  291. view.getSettings().setUseWideViewPort(enabled);
  292. }
  293. @ReactProp(name = "domStorageEnabled")
  294. public void setDomStorageEnabled(WebView view, boolean enabled) {
  295. view.getSettings().setDomStorageEnabled(enabled);
  296. }
  297. @ReactProp(name = "userAgent")
  298. public void setUserAgent(WebView view, @Nullable String userAgent) {
  299. if (userAgent != null) {
  300. // TODO(8496850): Fix incorrect behavior when property is unset (uA == null)
  301. view.getSettings().setUserAgentString(userAgent);
  302. }
  303. }
  304. @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
  305. @ReactProp(name = "mediaPlaybackRequiresUserAction")
  306. public void setMediaPlaybackRequiresUserAction(WebView view, boolean requires) {
  307. view.getSettings().setMediaPlaybackRequiresUserGesture(requires);
  308. }
  309. @ReactProp(name = "allowUniversalAccessFromFileURLs")
  310. public void setAllowUniversalAccessFromFileURLs(WebView view, boolean allow) {
  311. view.getSettings().setAllowUniversalAccessFromFileURLs(allow);
  312. }
  313. @ReactProp(name = "saveFormDataDisabled")
  314. public void setSaveFormDataDisabled(WebView view, boolean disable) {
  315. view.getSettings().setSaveFormData(!disable);
  316. }
  317. @ReactProp(name = "injectedJavaScript")
  318. public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScript) {
  319. ((RNCWebView) view).setInjectedJavaScript(injectedJavaScript);
  320. }
  321. @ReactProp(name = "messagingEnabled")
  322. public void setMessagingEnabled(WebView view, boolean enabled) {
  323. ((RNCWebView) view).setMessagingEnabled(enabled);
  324. }
  325. @ReactProp(name = "source")
  326. public void setSource(WebView view, @Nullable ReadableMap source) {
  327. if (source != null) {
  328. if (source.hasKey("html")) {
  329. String html = source.getString("html");
  330. String baseUrl = source.hasKey("baseUrl") ? source.getString("baseUrl") : "";
  331. view.loadDataWithBaseURL(baseUrl, html, HTML_MIME_TYPE, HTML_ENCODING, null);
  332. return;
  333. }
  334. if (source.hasKey("uri")) {
  335. String url = source.getString("uri");
  336. String previousUrl = view.getUrl();
  337. if (previousUrl != null && previousUrl.equals(url)) {
  338. return;
  339. }
  340. if (source.hasKey("method")) {
  341. String method = source.getString("method");
  342. if (method.equalsIgnoreCase(HTTP_METHOD_POST)) {
  343. byte[] postData = null;
  344. if (source.hasKey("body")) {
  345. String body = source.getString("body");
  346. try {
  347. postData = body.getBytes("UTF-8");
  348. } catch (UnsupportedEncodingException e) {
  349. postData = body.getBytes();
  350. }
  351. }
  352. if (postData == null) {
  353. postData = new byte[0];
  354. }
  355. view.postUrl(url, postData);
  356. return;
  357. }
  358. }
  359. HashMap<String, String> headerMap = new HashMap<>();
  360. if (source.hasKey("headers")) {
  361. ReadableMap headers = source.getMap("headers");
  362. ReadableMapKeySetIterator iter = headers.keySetIterator();
  363. while (iter.hasNextKey()) {
  364. String key = iter.nextKey();
  365. if ("user-agent".equals(key.toLowerCase(Locale.ENGLISH))) {
  366. if (view.getSettings() != null) {
  367. view.getSettings().setUserAgentString(headers.getString(key));
  368. }
  369. } else {
  370. headerMap.put(key, headers.getString(key));
  371. }
  372. }
  373. }
  374. view.loadUrl(url, headerMap);
  375. return;
  376. }
  377. }
  378. view.loadUrl(BLANK_URL);
  379. }
  380. @ReactProp(name = "onContentSizeChange")
  381. public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) {
  382. ((RNCWebView) view).setSendContentSizeChangeEvents(sendContentSizeChangeEvents);
  383. }
  384. @ReactProp(name = "mixedContentMode")
  385. public void setMixedContentMode(WebView view, @Nullable String mixedContentMode) {
  386. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  387. if (mixedContentMode == null || "never".equals(mixedContentMode)) {
  388. view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
  389. } else if ("always".equals(mixedContentMode)) {
  390. view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
  391. } else if ("compatibility".equals(mixedContentMode)) {
  392. view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
  393. }
  394. }
  395. }
  396. @ReactProp(name = "urlPrefixesForDefaultIntent")
  397. public void setUrlPrefixesForDefaultIntent(
  398. WebView view,
  399. @Nullable ReadableArray urlPrefixesForDefaultIntent) {
  400. RNCWebViewClient client = ((RNCWebView) view).getRNCWebViewClient();
  401. if (client != null && urlPrefixesForDefaultIntent != null) {
  402. client.setUrlPrefixesForDefaultIntent(urlPrefixesForDefaultIntent);
  403. }
  404. }
  405. @ReactProp(name = "allowFileAccess")
  406. public void setAllowFileAccess(
  407. WebView view,
  408. @Nullable Boolean allowFileAccess) {
  409. view.getSettings().setAllowFileAccess(allowFileAccess != null && allowFileAccess);
  410. }
  411. @ReactProp(name = "geolocationEnabled")
  412. public void setGeolocationEnabled(
  413. WebView view,
  414. @Nullable Boolean isGeolocationEnabled) {
  415. view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
  416. }
  417. @Override
  418. protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
  419. // Do not register default touch emitter and let WebView implementation handle touches
  420. view.setWebViewClient(new RNCWebViewClient());
  421. }
  422. @Override
  423. public Map getExportedCustomDirectEventTypeConstants() {
  424. Map export = super.getExportedCustomDirectEventTypeConstants();
  425. if (export == null) {
  426. export = MapBuilder.newHashMap();
  427. }
  428. export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress"));
  429. export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
  430. return export;
  431. }
  432. @Override
  433. public @Nullable
  434. Map<String, Integer> getCommandsMap() {
  435. return MapBuilder.of(
  436. "goBack", COMMAND_GO_BACK,
  437. "goForward", COMMAND_GO_FORWARD,
  438. "reload", COMMAND_RELOAD,
  439. "stopLoading", COMMAND_STOP_LOADING,
  440. "postMessage", COMMAND_POST_MESSAGE,
  441. "injectJavaScript", COMMAND_INJECT_JAVASCRIPT,
  442. "loadUrl", COMMAND_LOAD_URL
  443. );
  444. }
  445. @Override
  446. public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray args) {
  447. switch (commandId) {
  448. case COMMAND_GO_BACK:
  449. root.goBack();
  450. break;
  451. case COMMAND_GO_FORWARD:
  452. root.goForward();
  453. break;
  454. case COMMAND_RELOAD:
  455. root.reload();
  456. break;
  457. case COMMAND_STOP_LOADING:
  458. root.stopLoading();
  459. break;
  460. case COMMAND_POST_MESSAGE:
  461. try {
  462. RNCWebView reactWebView = (RNCWebView) root;
  463. JSONObject eventInitDict = new JSONObject();
  464. eventInitDict.put("data", args.getString(0));
  465. reactWebView.evaluateJavascriptWithFallback("(function () {" +
  466. "var event;" +
  467. "var data = " + eventInitDict.toString() + ";" +
  468. "try {" +
  469. "event = new MessageEvent('message', data);" +
  470. "} catch (e) {" +
  471. "event = document.createEvent('MessageEvent');" +
  472. "event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" +
  473. "}" +
  474. "document.dispatchEvent(event);" +
  475. "})();");
  476. } catch (JSONException e) {
  477. throw new RuntimeException(e);
  478. }
  479. break;
  480. case COMMAND_INJECT_JAVASCRIPT:
  481. RNCWebView reactWebView = (RNCWebView) root;
  482. reactWebView.evaluateJavascriptWithFallback(args.getString(0));
  483. break;
  484. case COMMAND_LOAD_URL:
  485. if (args == null) {
  486. throw new RuntimeException("Arguments for loading an url are null!");
  487. }
  488. root.loadUrl(args.getString(0));
  489. break;
  490. }
  491. }
  492. @Override
  493. public void onDropViewInstance(WebView webView) {
  494. super.onDropViewInstance(webView);
  495. ((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((RNCWebView) webView);
  496. ((RNCWebView) webView).cleanupCallbacksAndDestroy();
  497. }
  498. public RNCWebViewModule getModule(ReactContext reactContext) {
  499. return reactContext.getNativeModule(RNCWebViewModule.class);
  500. }
  501. protected static class RNCWebViewClient extends WebViewClient {
  502. protected boolean mLastLoadFailed = false;
  503. protected @Nullable
  504. ReadableArray mUrlPrefixesForDefaultIntent;
  505. @Override
  506. public void onPageFinished(WebView webView, String url) {
  507. super.onPageFinished(webView, url);
  508. if (!mLastLoadFailed) {
  509. RNCWebView reactWebView = (RNCWebView) webView;
  510. reactWebView.callInjectedJavaScript();
  511. emitFinishEvent(webView, url);
  512. }
  513. }
  514. @Override
  515. public void onPageStarted(WebView webView, String url, Bitmap favicon) {
  516. super.onPageStarted(webView, url, favicon);
  517. mLastLoadFailed = false;
  518. dispatchEvent(
  519. webView,
  520. new TopLoadingStartEvent(
  521. webView.getId(),
  522. createWebViewEvent(webView, url)));
  523. }
  524. @Override
  525. public boolean shouldOverrideUrlLoading(WebView view, String url) {
  526. dispatchEvent(
  527. view,
  528. new TopShouldStartLoadWithRequestEvent(
  529. view.getId(),
  530. createWebViewEvent(view, url)));
  531. return true;
  532. }
  533. @TargetApi(Build.VERSION_CODES.N)
  534. @Override
  535. public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
  536. final String url = request.getUrl().toString();
  537. return this.shouldOverrideUrlLoading(view, url);
  538. }
  539. @Override
  540. public void onReceivedError(
  541. WebView webView,
  542. int errorCode,
  543. String description,
  544. String failingUrl) {
  545. super.onReceivedError(webView, errorCode, description, failingUrl);
  546. mLastLoadFailed = true;
  547. // In case of an error JS side expect to get a finish event first, and then get an error event
  548. // Android WebView does it in the opposite way, so we need to simulate that behavior
  549. emitFinishEvent(webView, failingUrl);
  550. WritableMap eventData = createWebViewEvent(webView, failingUrl);
  551. eventData.putDouble("code", errorCode);
  552. eventData.putString("description", description);
  553. dispatchEvent(
  554. webView,
  555. new TopLoadingErrorEvent(webView.getId(), eventData));
  556. }
  557. protected void emitFinishEvent(WebView webView, String url) {
  558. dispatchEvent(
  559. webView,
  560. new TopLoadingFinishEvent(
  561. webView.getId(),
  562. createWebViewEvent(webView, url)));
  563. }
  564. protected WritableMap createWebViewEvent(WebView webView, String url) {
  565. WritableMap event = Arguments.createMap();
  566. event.putDouble("target", webView.getId());
  567. // Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks
  568. // like onPageFinished
  569. event.putString("url", url);
  570. event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100);
  571. event.putString("title", webView.getTitle());
  572. event.putBoolean("canGoBack", webView.canGoBack());
  573. event.putBoolean("canGoForward", webView.canGoForward());
  574. return event;
  575. }
  576. public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
  577. mUrlPrefixesForDefaultIntent = specialUrls;
  578. }
  579. }
  580. /**
  581. * Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order
  582. * to call {@link WebView#destroy} on activity destroy event and also to clear the client
  583. */
  584. protected static class RNCWebView extends WebView implements LifecycleEventListener {
  585. protected @Nullable
  586. String injectedJS;
  587. protected boolean messagingEnabled = false;
  588. protected @Nullable
  589. RNCWebViewClient mRNCWebViewClient;
  590. protected boolean sendContentSizeChangeEvents = false;
  591. /**
  592. * WebView must be created with an context of the current activity
  593. * <p>
  594. * Activity Context is required for creation of dialogs internally by WebView
  595. * Reactive Native needed for access to ReactNative internal system functionality
  596. */
  597. public RNCWebView(ThemedReactContext reactContext) {
  598. super(reactContext);
  599. }
  600. public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
  601. this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
  602. }
  603. @Override
  604. public void onHostResume() {
  605. // do nothing
  606. }
  607. @Override
  608. public void onHostPause() {
  609. // do nothing
  610. }
  611. @Override
  612. public void onHostDestroy() {
  613. cleanupCallbacksAndDestroy();
  614. }
  615. @Override
  616. protected void onSizeChanged(int w, int h, int ow, int oh) {
  617. super.onSizeChanged(w, h, ow, oh);
  618. if (sendContentSizeChangeEvents) {
  619. dispatchEvent(
  620. this,
  621. new ContentSizeChangeEvent(
  622. this.getId(),
  623. w,
  624. h
  625. )
  626. );
  627. }
  628. }
  629. @Override
  630. public void setWebViewClient(WebViewClient client) {
  631. super.setWebViewClient(client);
  632. mRNCWebViewClient = (RNCWebViewClient) client;
  633. }
  634. public @Nullable
  635. RNCWebViewClient getRNCWebViewClient() {
  636. return mRNCWebViewClient;
  637. }
  638. public void setInjectedJavaScript(@Nullable String js) {
  639. injectedJS = js;
  640. }
  641. protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
  642. return new RNCWebViewBridge(webView);
  643. }
  644. @SuppressLint("AddJavascriptInterface")
  645. public void setMessagingEnabled(boolean enabled) {
  646. if (messagingEnabled == enabled) {
  647. return;
  648. }
  649. messagingEnabled = enabled;
  650. if (enabled) {
  651. addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
  652. } else {
  653. removeJavascriptInterface(JAVASCRIPT_INTERFACE);
  654. }
  655. }
  656. protected void evaluateJavascriptWithFallback(String script) {
  657. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
  658. evaluateJavascript(script, null);
  659. return;
  660. }
  661. try {
  662. loadUrl("javascript:" + URLEncoder.encode(script, "UTF-8"));
  663. } catch (UnsupportedEncodingException e) {
  664. // UTF-8 should always be supported
  665. throw new RuntimeException(e);
  666. }
  667. }
  668. public void callInjectedJavaScript() {
  669. if (getSettings().getJavaScriptEnabled() &&
  670. injectedJS != null &&
  671. !TextUtils.isEmpty(injectedJS)) {
  672. evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();");
  673. }
  674. }
  675. public void onMessage(String message) {
  676. dispatchEvent(this, new TopMessageEvent(this.getId(), message));
  677. }
  678. protected void cleanupCallbacksAndDestroy() {
  679. setWebViewClient(null);
  680. destroy();
  681. }
  682. protected class RNCWebViewBridge {
  683. RNCWebView mContext;
  684. RNCWebViewBridge(RNCWebView c) {
  685. mContext = c;
  686. }
  687. /**
  688. * This method is called whenever JavaScript running within the web view calls:
  689. * - window[JAVASCRIPT_INTERFACE].postMessage
  690. */
  691. @JavascriptInterface
  692. public void postMessage(String message) {
  693. mContext.onMessage(message);
  694. }
  695. }
  696. }
  697. }