react-native-webview.git

RNCWebViewManager.java 32KB

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