react-native-webview.git

RNCWebViewManager.java 37KB

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