2018-08-15 20:30:06 -07:00
package com.reactnativecommunity.webview ;
2018-07-30 20:21:48 -07:00
2020-04-04 16:54:36 +03:00
import java.io.ByteArrayInputStream ;
import java.io.IOException ;
import java.io.InputStream ;
import java.nio.charset.Charset ;
import java.nio.charset.StandardCharsets ;
import java.nio.charset.UnsupportedCharsetException ;
import okhttp3.MediaType ;
import okhttp3.OkHttpClient ;
import okhttp3.OkHttpClient.Builder ;
import okhttp3.Request ;
import okhttp3.Response ;
import org.json.JSONException ;
import org.json.JSONObject ;
import java.net.HttpURLConnection ;
import static okhttp3.internal.Util.UTF_8 ;
import android.util.Log ;
2019-01-30 09:32:46 +00:00
import android.annotation.SuppressLint ;
2018-07-30 20:21:48 -07:00
import android.annotation.TargetApi ;
2019-01-07 06:02:47 -08:00
import android.app.DownloadManager ;
2018-07-30 20:21:48 -07:00
import android.content.Context ;
import android.content.Intent ;
2019-08-29 09:38:25 -05:00
import android.content.pm.ActivityInfo ;
2019-08-02 12:23:59 -03:00
import android.content.pm.PackageManager ;
2018-07-30 20:21:48 -07:00
import android.graphics.Bitmap ;
2019-05-16 15:22:08 -07:00
import android.graphics.Color ;
2019-08-02 12:23:59 -03:00
import android.Manifest ;
2018-07-30 20:21:48 -07:00
import android.net.Uri ;
import android.os.Build ;
2019-01-07 06:02:47 -08:00
import android.os.Environment ;
2019-05-20 00:36:51 +02:00
import androidx.annotation.RequiresApi ;
2019-08-02 12:23:59 -03:00
import androidx.core.content.ContextCompat ;
2018-07-30 20:21:48 -07:00
import android.text.TextUtils ;
2019-05-16 15:22:08 -07:00
import android.view.Gravity ;
2018-10-17 16:48:19 +02:00
import android.view.View ;
2019-05-16 15:22:08 -07:00
import android.view.ViewGroup ;
2018-07-30 20:21:48 -07:00
import android.view.ViewGroup.LayoutParams ;
2019-05-16 15:22:08 -07:00
import android.view.WindowManager ;
2018-07-30 20:21:48 -07:00
import android.webkit.ConsoleMessage ;
import android.webkit.CookieManager ;
2019-01-07 06:02:47 -08:00
import android.webkit.DownloadListener ;
2018-07-30 20:21:48 -07:00
import android.webkit.GeolocationPermissions ;
import android.webkit.JavascriptInterface ;
2019-08-02 12:23:59 -03:00
import android.webkit.PermissionRequest ;
2019-01-07 06:02:47 -08:00
import android.webkit.URLUtil ;
2020-04-04 16:54:36 +03:00
import android.webkit.ServiceWorkerController ;
import android.webkit.ServiceWorkerClient ;
2018-07-30 20:21:48 -07:00
import android.webkit.ValueCallback ;
import android.webkit.WebChromeClient ;
2018-11-30 02:59:12 +01:00
import android.webkit.WebResourceRequest ;
2019-09-20 17:43:28 -07:00
import android.webkit.WebResourceResponse ;
2018-07-30 20:21:48 -07:00
import android.webkit.WebSettings ;
import android.webkit.WebView ;
import android.webkit.WebViewClient ;
2019-05-16 15:22:08 -07:00
import android.widget.FrameLayout ;
2019-01-07 06:02:47 -08:00
2019-05-16 18:27:16 -04:00
import com.facebook.react.views.scroll.ScrollEvent ;
import com.facebook.react.views.scroll.ScrollEventType ;
import com.facebook.react.views.scroll.OnScrollDispatchHelper ;
2018-07-30 20:21:48 -07:00
import com.facebook.react.bridge.Arguments ;
2020-04-06 20:25:28 +03:00
import com.facebook.react.bridge.CatalystInstance ;
2018-07-30 20:21:48 -07:00
import com.facebook.react.bridge.LifecycleEventListener ;
import com.facebook.react.bridge.ReactContext ;
import com.facebook.react.bridge.ReadableArray ;
import com.facebook.react.bridge.ReadableMap ;
import com.facebook.react.bridge.ReadableMapKeySetIterator ;
import com.facebook.react.bridge.WritableMap ;
2020-04-06 20:25:28 +03:00
import com.facebook.react.bridge.WritableNativeArray ;
import com.facebook.react.bridge.WritableNativeMap ;
2018-07-30 20:21:48 -07:00
import com.facebook.react.common.MapBuilder ;
import com.facebook.react.common.build.ReactBuildConfig ;
import com.facebook.react.module.annotations.ReactModule ;
import com.facebook.react.uimanager.SimpleViewManager ;
import com.facebook.react.uimanager.ThemedReactContext ;
2019-03-19 15:42:47 +01:00
import com.facebook.react.uimanager.UIManagerModule ;
2018-07-30 20:21:48 -07:00
import com.facebook.react.uimanager.annotations.ReactProp ;
import com.facebook.react.uimanager.events.ContentSizeChangeEvent ;
import com.facebook.react.uimanager.events.Event ;
import com.facebook.react.uimanager.events.EventDispatcher ;
2018-08-15 20:30:06 -07:00
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent ;
2019-09-20 17:43:28 -07:00
import com.reactnativecommunity.webview.events.TopHttpErrorEvent ;
2018-08-15 20:30:06 -07:00
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent ;
2019-03-19 15:42:47 +01:00
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent ;
2018-08-15 20:30:06 -07:00
import com.reactnativecommunity.webview.events.TopLoadingStartEvent ;
import com.reactnativecommunity.webview.events.TopMessageEvent ;
2018-11-30 02:59:12 +01:00
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent ;
2019-03-19 15:42:47 +01:00
2018-07-30 20:21:48 -07:00
import org.json.JSONException ;
import org.json.JSONObject ;
2019-03-19 15:42:47 +01:00
import java.io.UnsupportedEncodingException ;
import java.net.MalformedURLException ;
import java.net.URL ;
import java.net.URLEncoder ;
2019-08-02 12:23:59 -03:00
import java.util.ArrayList ;
2019-03-19 15:42:47 +01:00
import java.util.HashMap ;
import java.util.Locale ;
import java.util.Map ;
import javax.annotation.Nullable ;
2018-07-30 20:21:48 -07:00
/**
* Manages instances of {@link WebView}
2019-03-19 15:42:47 +01:00
* <p>
2018-07-30 20:21:48 -07:00
* Can accept following commands:
2019-03-19 15:42:47 +01:00
* - GO_BACK
* - GO_FORWARD
* - RELOAD
* - LOAD_URL
* <p>
2018-07-30 20:21:48 -07:00
* {@link WebView} instances could emit following direct events:
2019-03-19 15:42:47 +01:00
* - topLoadingFinish
* - topLoadingStart
* - topLoadingStart
* - topLoadingProgress
* - topShouldStartLoadWithRequest
* <p>
2018-07-30 20:21:48 -07:00
* Each event will carry the following properties:
2019-03-19 15:42:47 +01:00
* - target - view's react tag
* - url - url set for the webview
* - loading - whether webview is in a loading state
* - title - title of the current page
* - canGoBack - boolean, whether there is anything on a history stack to go back
* - canGoForward - boolean, whether it is possible to request GO_FORWARD command
2018-07-30 20:21:48 -07:00
*/
2018-09-19 01:19:06 +02:00
@ReactModule ( name = RNCWebViewManager . REACT_CLASS )
public class RNCWebViewManager extends SimpleViewManager < WebView > {
2018-07-30 20:21:48 -07:00
2019-09-23 01:35:49 -07:00
public static String activeUrl = null ;
2018-07-30 20:21:48 -07:00
public static final int COMMAND_GO_BACK = 1 ;
public static final int COMMAND_GO_FORWARD = 2 ;
public static final int COMMAND_RELOAD = 3 ;
public static final int COMMAND_STOP_LOADING = 4 ;
public static final int COMMAND_POST_MESSAGE = 5 ;
public static final int COMMAND_INJECT_JAVASCRIPT = 6 ;
2018-11-30 02:59:12 +01:00
public static final int COMMAND_LOAD_URL = 7 ;
2019-08-06 11:56:59 +03:00
public static final int COMMAND_FOCUS = 8 ;
2019-11-12 12:09:16 +03:00
// android commands
public static final int COMMAND_CLEAR_FORM_DATA = 1000 ;
public static final int COMMAND_CLEAR_CACHE = 1001 ;
public static final int COMMAND_CLEAR_HISTORY = 1002 ;
2019-03-19 15:42:47 +01:00
protected static final String REACT_CLASS = "RNCWebView" ;
2020-04-04 16:54:36 +03:00
protected static final String HEADER_CONTENT_TYPE = "content-type" ;
2019-03-19 15:42:47 +01:00
protected static final String HTML_ENCODING = "UTF-8" ;
protected static final String HTML_MIME_TYPE = "text/html" ;
2020-04-04 16:54:36 +03:00
protected static final String UNKNOWN_MIME_TYPE = "application/octet-stream" ;
2019-03-19 15:42:47 +01:00
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView" ;
protected static final String HTTP_METHOD_POST = "POST" ;
2018-07-30 20:21:48 -07:00
// Use `webView.loadUrl("about:blank")` to reliably reset the view
// state and release page resources (including any running JavaScript).
protected static final String BLANK_URL = "about:blank" ;
protected WebViewConfig mWebViewConfig ;
2019-05-16 15:22:08 -07:00
protected RNCWebChromeClient mWebChromeClient = null ;
protected boolean mAllowsFullscreenVideo = false ;
2019-08-02 04:46:03 -04:00
protected @Nullable String mUserAgent = null ;
protected @Nullable String mUserAgentWithApplicationName = null ;
2020-04-04 16:54:36 +03:00
protected static String userAgent ;
protected static OkHttpClient httpClient ;
2019-05-16 15:22:08 -07:00
2018-09-19 01:19:06 +02:00
public RNCWebViewManager () {
2018-07-30 20:21:48 -07:00
mWebViewConfig = new WebViewConfig () {
public void configWebView ( WebView webView ) {
}
};
2020-04-04 16:54:36 +03:00
httpClient = new Builder ()
. followRedirects ( false )
. followSslRedirects ( false )
. build ();
2018-07-30 20:21:48 -07:00
}
2018-09-19 01:19:06 +02:00
public RNCWebViewManager ( WebViewConfig webViewConfig ) {
2018-07-30 20:21:48 -07:00
mWebViewConfig = webViewConfig ;
}
2019-03-19 15:42:47 +01:00
protected static void dispatchEvent ( WebView webView , Event event ) {
ReactContext reactContext = ( ReactContext ) webView . getContext ();
EventDispatcher eventDispatcher =
reactContext . getNativeModule ( UIManagerModule . class ). getEventDispatcher ();
eventDispatcher . dispatchEvent ( event );
}
2018-07-30 20:21:48 -07:00
@Override
public String getName () {
return REACT_CLASS ;
}
2018-09-19 01:19:06 +02:00
protected RNCWebView createRNCWebViewInstance ( ThemedReactContext reactContext ) {
return new RNCWebView ( reactContext );
2018-07-30 20:21:48 -07:00
}
@Override
@TargetApi ( Build . VERSION_CODES . LOLLIPOP )
protected WebView createViewInstance ( ThemedReactContext reactContext ) {
2018-09-19 01:19:06 +02:00
RNCWebView webView = createRNCWebViewInstance ( reactContext );
2020-04-04 16:54:36 +03:00
userAgent = webView . getSettings (). getUserAgentString ();
2019-05-16 15:22:08 -07:00
setupWebChromeClient ( reactContext , webView );
2018-07-30 20:21:48 -07:00
reactContext . addLifecycleEventListener ( webView );
mWebViewConfig . configWebView ( webView );
WebSettings settings = webView . getSettings ();
settings . setBuiltInZoomControls ( true );
settings . setDisplayZoomControls ( false );
settings . setDomStorageEnabled ( true );
settings . setAllowFileAccess ( false );
settings . setAllowContentAccess ( false );
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . JELLY_BEAN ) {
settings . setAllowFileAccessFromFileURLs ( false );
setAllowUniversalAccessFromFileURLs ( webView , false );
}
setMixedContentMode ( webView , "never" );
// Fixes broken full-screen modals/galleries due to body height being 0.
webView . setLayoutParams (
2019-03-19 15:42:47 +01:00
new LayoutParams ( LayoutParams . MATCH_PARENT ,
LayoutParams . MATCH_PARENT ));
2018-07-30 20:21:48 -07:00
if ( ReactBuildConfig . DEBUG && Build . VERSION . SDK_INT >= Build . VERSION_CODES . KITKAT ) {
WebView . setWebContentsDebuggingEnabled ( true );
}
2019-01-07 06:02:47 -08:00
webView . setDownloadListener ( new DownloadListener () {
public void onDownloadStart ( String url , String userAgent , String contentDisposition , String mimetype , long contentLength ) {
2020-04-10 13:41:53 -07:00
webView . setIgnoreErrFailedForThisURL ( url );
2019-03-20 14:04:47 +01:00
RNCWebViewModule module = getModule ( reactContext );
2019-01-07 06:02:47 -08:00
DownloadManager . Request request = new DownloadManager . Request ( Uri . parse ( url ));
2019-01-29 10:05:25 +01:00
String fileName = URLUtil . guessFileName ( url , contentDisposition , mimetype );
2019-01-07 06:02:47 -08:00
String downloadMessage = "Downloading " + fileName ;
//Attempt to add cookie, if it exists
URL urlObj = null ;
2019-01-11 10:44:42 +01:00
try {
2019-01-07 06:02:47 -08:00
urlObj = new URL ( url );
String baseUrl = urlObj . getProtocol () + "://" + urlObj . getHost ();
2019-01-11 10:44:42 +01:00
String cookie = CookieManager . getInstance (). getCookie ( baseUrl );
2019-01-07 06:02:47 -08:00
request . addRequestHeader ( "Cookie" , cookie );
} catch ( MalformedURLException e ) {
System . out . println ( "Error getting cookie for DownloadManager: " + e . toString ());
2019-01-11 10:44:42 +01:00
e . printStackTrace ();
2019-01-07 06:02:47 -08:00
}
//Finish setting up request
request . addRequestHeader ( "User-Agent" , userAgent );
request . setTitle ( fileName );
request . setDescription ( downloadMessage );
request . allowScanningByMediaScanner ();
request . setNotificationVisibility ( DownloadManager . Request . VISIBILITY_VISIBLE_NOTIFY_COMPLETED );
request . setDestinationInExternalPublicDir ( Environment . DIRECTORY_DOWNLOADS , fileName );
module . setDownloadRequest ( request );
if ( module . grantFileDownloaderPermissions ()) {
module . downloadFile ();
}
}
});
2020-04-04 16:54:36 +03:00
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . N ) {
ServiceWorkerController swController = ServiceWorkerController . getInstance ();
swController . setServiceWorkerClient ( new ServiceWorkerClient () {
@Override
public WebResourceResponse shouldInterceptRequest ( WebResourceRequest request ) {
Log . d ( REACT_CLASS , "shouldInterceptRequest / ServiceWorkerClient" );
WebResourceResponse response = RNCWebViewManager . this . shouldInterceptRequest ( request , false , webView );
if ( response != null ) {
Log . d ( REACT_CLASS , "shouldInterceptRequest / ServiceWorkerClient -> return intersept response" );
return response ;
}
Log . d ( REACT_CLASS , "shouldInterceptRequest / ServiceWorkerClient -> intercept response is nil, delegating up" );
return super . shouldInterceptRequest ( request );
}
});
}
2018-07-30 20:21:48 -07:00
return webView ;
}
2020-04-04 16:54:36 +03:00
private Boolean urlStringLooksInvalid ( String urlString ) {
return urlString == null ||
urlString . trim (). equals ( "" ) ||
! ( urlString . startsWith ( "http" ) && ! urlString . startsWith ( "www" )) ||
urlString . contains ( "|" );
}
private Boolean responseRequiresJSInjection ( Response response ) {
// we don't want to inject JS into redirects
if ( response . isRedirect ()) {
return false ;
}
// ...okhttp appends charset to content type sometimes, like "text/html; charset=UTF8"
final String contentTypeAndCharset = response . header ( HEADER_CONTENT_TYPE , UNKNOWN_MIME_TYPE );
// ...and we only want to inject it in to HTML, really
return contentTypeAndCharset . startsWith ( HTML_MIME_TYPE );
}
public WebResourceResponse shouldInterceptRequest ( WebResourceRequest request , Boolean onlyMainFrame , RNCWebView webView ) {
Uri url = request . getUrl ();
String urlStr = url . toString ();
Log . i ( "StatusNativeLogs" , "###shouldInterceptRequest 1" );
Log . d ( REACT_CLASS , "new request " );
Log . d ( REACT_CLASS , "url " + urlStr );
Log . d ( REACT_CLASS , "host " + request . getUrl (). getHost ());
Log . d ( REACT_CLASS , "path " + request . getUrl (). getPath ());
Log . d ( REACT_CLASS , "main " + request . isForMainFrame ());
Log . d ( REACT_CLASS , "headers " + request . getRequestHeaders (). toString ());
Log . d ( REACT_CLASS , "method " + request . getMethod ());
Log . i ( "StatusNativeLogs" , "###shouldInterceptRequest 2" );
if ( onlyMainFrame && ! request . isForMainFrame () ||
urlStringLooksInvalid ( urlStr )) {
return null ; //super.shouldInterceptRequest(webView, request);
}
Log . i ( "StatusNativeLogs" , "###shouldInterceptRequest 3" );
try {
Log . i ( "StatusNativeLogs" , "###shouldInterceptRequest 4" );
Request req = new Request . Builder ()
. url ( urlStr )
. header ( "User-Agent" , userAgent )
. build ();
Log . i ( "StatusNativeLogs" , "### httpCall " + new Boolean ( httpClient != null ). toString ());
Response response = httpClient . newCall ( req ). execute ();
Log . d ( REACT_CLASS , "response headers " + response . headers (). toString ());
Log . d ( REACT_CLASS , "response code " + response . code ());
Log . d ( REACT_CLASS , "response suc " + response . isSuccessful ());
if ( ! responseRequiresJSInjection ( response )) {
return null ;
}
InputStream is = response . body (). byteStream ();
MediaType contentType = response . body (). contentType ();
Charset charset = contentType != null ? contentType . charset ( UTF_8 ) : UTF_8 ;
RNCWebView reactWebView = ( RNCWebView ) webView ;
if ( response . code () == HttpURLConnection . HTTP_OK ) {
is = new InputStreamWithInjectedJS ( is , reactWebView . injectedJSBeforeContentLoaded , charset );
}
Log . d ( REACT_CLASS , "inject our custom JS to this request" );
return new WebResourceResponse ( "text/html" , charset . name (), is );
} catch ( IOException e ) {
return null ;
}
}
2018-07-30 20:21:48 -07:00
@ReactProp ( name = "javaScriptEnabled" )
public void setJavaScriptEnabled ( WebView view , boolean enabled ) {
view . getSettings (). setJavaScriptEnabled ( enabled );
}
2019-02-12 07:47:24 -06:00
@ReactProp ( name = "showsHorizontalScrollIndicator" )
public void setShowsHorizontalScrollIndicator ( WebView view , boolean enabled ) {
view . setHorizontalScrollBarEnabled ( enabled );
}
@ReactProp ( name = "showsVerticalScrollIndicator" )
public void setShowsVerticalScrollIndicator ( WebView view , boolean enabled ) {
view . setVerticalScrollBarEnabled ( enabled );
}
2019-03-19 15:42:47 +01:00
2019-01-30 09:32:46 +00:00
@ReactProp ( name = "cacheEnabled" )
public void setCacheEnabled ( WebView view , boolean enabled ) {
if ( enabled ) {
Context ctx = view . getContext ();
if ( ctx != null ) {
view . getSettings (). setAppCachePath ( ctx . getCacheDir (). getAbsolutePath ());
view . getSettings (). setCacheMode ( WebSettings . LOAD_DEFAULT );
view . getSettings (). setAppCacheEnabled ( true );
}
} else {
view . getSettings (). setCacheMode ( WebSettings . LOAD_NO_CACHE );
view . getSettings (). setAppCacheEnabled ( false );
}
}
2019-09-29 23:45:20 +10:00
@ReactProp ( name = "cacheMode" )
public void setCacheMode ( WebView view , String cacheModeString ) {
Integer cacheMode ;
switch ( cacheModeString ) {
case "LOAD_CACHE_ONLY" :
cacheMode = WebSettings . LOAD_CACHE_ONLY ;
break ;
case "LOAD_CACHE_ELSE_NETWORK" :
cacheMode = WebSettings . LOAD_CACHE_ELSE_NETWORK ;
2019-11-12 12:09:16 +03:00
break ;
2019-09-29 23:45:20 +10:00
case "LOAD_NO_CACHE" :
cacheMode = WebSettings . LOAD_NO_CACHE ;
break ;
case "LOAD_DEFAULT" :
default :
cacheMode = WebSettings . LOAD_DEFAULT ;
break ;
}
view . getSettings (). setCacheMode ( cacheMode );
}
2019-01-22 17:21:10 +08:00
@ReactProp ( name = "androidHardwareAccelerationDisabled" )
public void setHardwareAccelerationDisabled ( WebView view , boolean disabled ) {
2019-10-22 14:56:50 +07:00
if ( disabled ) {
2019-01-22 17:21:10 +08:00
view . setLayerType ( View . LAYER_TYPE_SOFTWARE , null );
}
}
2018-10-17 16:48:19 +02:00
@ReactProp ( name = "overScrollMode" )
public void setOverScrollMode ( WebView view , String overScrollModeString ) {
Integer overScrollMode ;
switch ( overScrollModeString ) {
case "never" :
overScrollMode = View . OVER_SCROLL_NEVER ;
break ;
case "content" :
overScrollMode = View . OVER_SCROLL_IF_CONTENT_SCROLLS ;
break ;
case "always" :
default :
overScrollMode = View . OVER_SCROLL_ALWAYS ;
break ;
}
view . setOverScrollMode ( overScrollMode );
}
2018-07-30 20:21:48 -07:00
@ReactProp ( name = "thirdPartyCookiesEnabled" )
public void setThirdPartyCookiesEnabled ( WebView view , boolean enabled ) {
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . LOLLIPOP ) {
CookieManager . getInstance (). setAcceptThirdPartyCookies ( view , enabled );
}
}
2019-03-28 11:52:18 +03:00
@ReactProp ( name = "textZoom" )
public void setTextZoom ( WebView view , int value ) {
view . getSettings (). setTextZoom ( value );
}
2018-07-30 20:21:48 -07:00
@ReactProp ( name = "scalesPageToFit" )
public void setScalesPageToFit ( WebView view , boolean enabled ) {
2019-03-01 05:02:44 +08:00
view . getSettings (). setLoadWithOverviewMode ( enabled );
view . getSettings (). setUseWideViewPort ( enabled );
2018-07-30 20:21:48 -07:00
}
@ReactProp ( name = "domStorageEnabled" )
public void setDomStorageEnabled ( WebView view , boolean enabled ) {
view . getSettings (). setDomStorageEnabled ( enabled );
}
@ReactProp ( name = "userAgent" )
public void setUserAgent ( WebView view , @Nullable String userAgent ) {
if ( userAgent != null ) {
2019-08-02 04:46:03 -04:00
mUserAgent = userAgent ;
} else {
mUserAgent = null ;
}
this . setUserAgentString ( view );
}
@ReactProp ( name = "applicationNameForUserAgent" )
public void setApplicationNameForUserAgent ( WebView view , @Nullable String applicationName ) {
if ( applicationName != null ) {
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . JELLY_BEAN_MR1 ) {
String defaultUserAgent = WebSettings . getDefaultUserAgent ( view . getContext ());
mUserAgentWithApplicationName = defaultUserAgent + " " + applicationName ;
}
} else {
mUserAgentWithApplicationName = null ;
}
this . setUserAgentString ( view );
}
protected void setUserAgentString ( WebView view ) {
if ( mUserAgent != null ) {
view . getSettings (). setUserAgentString ( mUserAgent );
} else if ( mUserAgentWithApplicationName != null ) {
view . getSettings (). setUserAgentString ( mUserAgentWithApplicationName );
} else if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . JELLY_BEAN_MR1 ) {
// handle unsets of `userAgent` prop as long as device is >= API 17
view . getSettings (). setUserAgentString ( WebSettings . getDefaultUserAgent ( view . getContext ()));
2018-07-30 20:21:48 -07:00
}
}
2018-11-19 11:25:28 +01:00
@TargetApi ( Build . VERSION_CODES . JELLY_BEAN_MR1 )
2018-07-30 20:21:48 -07:00
@ReactProp ( name = "mediaPlaybackRequiresUserAction" )
public void setMediaPlaybackRequiresUserAction ( WebView view , boolean requires ) {
view . getSettings (). setMediaPlaybackRequiresUserGesture ( requires );
}
2019-09-29 15:50:39 +02:00
@ReactProp ( name = "allowFileAccessFromFileURLs" )
public void setAllowFileAccessFromFileURLs ( WebView view , boolean allow ) {
view . getSettings (). setAllowFileAccessFromFileURLs ( allow );
}
2018-07-30 20:21:48 -07:00
@ReactProp ( name = "allowUniversalAccessFromFileURLs" )
public void setAllowUniversalAccessFromFileURLs ( WebView view , boolean allow ) {
view . getSettings (). setAllowUniversalAccessFromFileURLs ( allow );
}
@ReactProp ( name = "saveFormDataDisabled" )
public void setSaveFormDataDisabled ( WebView view , boolean disable ) {
view . getSettings (). setSaveFormData ( ! disable );
}
@ReactProp ( name = "injectedJavaScript" )
public void setInjectedJavaScript ( WebView view , @Nullable String injectedJavaScript ) {
2018-09-19 01:19:06 +02:00
(( RNCWebView ) view ). setInjectedJavaScript ( injectedJavaScript );
2018-07-30 20:21:48 -07:00
}
2020-02-11 15:06:06 +02:00
@ReactProp ( name = "injectedJavaScriptBeforeContentLoaded" )
public void setInjectedJavaScriptBeforeContentLoaded ( WebView view , @Nullable String injectedJavaScriptBeforeContentLoaded ) {
(( RNCWebView ) view ). setInjectedJavaScriptBeforeContentLoaded ( injectedJavaScriptBeforeContentLoaded );
}
2018-07-30 20:21:48 -07:00
@ReactProp ( name = "messagingEnabled" )
public void setMessagingEnabled ( WebView view , boolean enabled ) {
2018-09-19 01:19:06 +02:00
(( RNCWebView ) view ). setMessagingEnabled ( enabled );
2018-07-30 20:21:48 -07:00
}
2020-04-09 18:05:44 +03:00
@ReactProp ( name = "messagingModuleName" )
public void setMessagingModuleName ( WebView view , String moduleName ) {
(( RNCWebView ) view ). setMessagingModuleName ( moduleName );
}
2019-09-23 15:40:40 +02:00
2019-08-02 00:48:54 -07:00
@ReactProp ( name = "incognito" )
public void setIncognito ( WebView view , boolean enabled ) {
// Remove all previous cookies
2019-08-29 22:47:48 +08:00
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . LOLLIPOP ) {
CookieManager . getInstance (). removeAllCookies ( null );
} else {
CookieManager . getInstance (). removeAllCookie ();
}
2019-08-02 00:48:54 -07:00
// Disable caching
view . getSettings (). setCacheMode ( WebSettings . LOAD_NO_CACHE );
view . getSettings (). setAppCacheEnabled ( ! enabled );
view . clearHistory ();
view . clearCache ( enabled );
// No form data or autofill enabled
view . clearFormData ();
view . getSettings (). setSavePassword ( ! enabled );
view . getSettings (). setSaveFormData ( ! enabled );
}
2018-07-30 20:21:48 -07:00
@ReactProp ( name = "source" )
public void setSource ( WebView view , @Nullable ReadableMap source ) {
if ( source != null ) {
if ( source . hasKey ( "html" )) {
String html = source . getString ( "html" );
2019-04-15 17:56:45 +10:00
String baseUrl = source . hasKey ( "baseUrl" ) ? source . getString ( "baseUrl" ) : "" ;
view . loadDataWithBaseURL ( baseUrl , html , HTML_MIME_TYPE , HTML_ENCODING , null );
2018-07-30 20:21:48 -07:00
return ;
}
if ( source . hasKey ( "uri" )) {
String url = source . getString ( "uri" );
String previousUrl = view . getUrl ();
if ( previousUrl != null && previousUrl . equals ( url )) {
return ;
}
if ( source . hasKey ( "method" )) {
String method = source . getString ( "method" );
2018-10-13 00:40:20 +02:00
if ( method . equalsIgnoreCase ( HTTP_METHOD_POST )) {
2018-07-30 20:21:48 -07:00
byte [] postData = null ;
if ( source . hasKey ( "body" )) {
String body = source . getString ( "body" );
try {
postData = body . getBytes ( "UTF-8" );
} catch ( UnsupportedEncodingException e ) {
postData = body . getBytes ();
}
}
if ( postData == null ) {
postData = new byte [ 0 ] ;
}
view . postUrl ( url , postData );
return ;
}
}
HashMap < String , String > headerMap = new HashMap <> ();
if ( source . hasKey ( "headers" )) {
ReadableMap headers = source . getMap ( "headers" );
ReadableMapKeySetIterator iter = headers . keySetIterator ();
while ( iter . hasNextKey ()) {
String key = iter . nextKey ();
if ( "user-agent" . equals ( key . toLowerCase ( Locale . ENGLISH ))) {
if ( view . getSettings () != null ) {
view . getSettings (). setUserAgentString ( headers . getString ( key ));
}
} else {
headerMap . put ( key , headers . getString ( key ));
}
}
}
view . loadUrl ( url , headerMap );
return ;
}
}
view . loadUrl ( BLANK_URL );
}
@ReactProp ( name = "onContentSizeChange" )
public void setOnContentSizeChange ( WebView view , boolean sendContentSizeChangeEvents ) {
2019-01-10 10:38:05 +00:00
(( RNCWebView ) view ). setSendContentSizeChangeEvents ( sendContentSizeChangeEvents );
2018-07-30 20:21:48 -07:00
}
@ReactProp ( name = "mixedContentMode" )
public void setMixedContentMode ( WebView view , @Nullable String mixedContentMode ) {
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . LOLLIPOP ) {
if ( mixedContentMode == null || "never" . equals ( mixedContentMode )) {
view . getSettings (). setMixedContentMode ( WebSettings . MIXED_CONTENT_NEVER_ALLOW );
} else if ( "always" . equals ( mixedContentMode )) {
view . getSettings (). setMixedContentMode ( WebSettings . MIXED_CONTENT_ALWAYS_ALLOW );
} else if ( "compatibility" . equals ( mixedContentMode )) {
view . getSettings (). setMixedContentMode ( WebSettings . MIXED_CONTENT_COMPATIBILITY_MODE );
}
}
}
@ReactProp ( name = "urlPrefixesForDefaultIntent" )
public void setUrlPrefixesForDefaultIntent (
2019-03-19 15:42:47 +01:00
WebView view ,
@Nullable ReadableArray urlPrefixesForDefaultIntent ) {
2018-09-19 01:19:06 +02:00
RNCWebViewClient client = (( RNCWebView ) view ). getRNCWebViewClient ();
2018-07-30 20:21:48 -07:00
if ( client != null && urlPrefixesForDefaultIntent != null ) {
client . setUrlPrefixesForDefaultIntent ( urlPrefixesForDefaultIntent );
}
}
2019-05-16 15:22:08 -07:00
@ReactProp ( name = "allowsFullscreenVideo" )
public void setAllowsFullscreenVideo (
WebView view ,
@Nullable Boolean allowsFullscreenVideo ) {
mAllowsFullscreenVideo = allowsFullscreenVideo != null && allowsFullscreenVideo ;
setupWebChromeClient (( ReactContext ) view . getContext (), view );
}
2018-09-14 01:09:39 +02:00
@ReactProp ( name = "allowFileAccess" )
public void setAllowFileAccess (
WebView view ,
@Nullable Boolean allowFileAccess ) {
view . getSettings (). setAllowFileAccess ( allowFileAccess != null && allowFileAccess );
}
2018-07-30 20:21:48 -07:00
@ReactProp ( name = "geolocationEnabled" )
public void setGeolocationEnabled (
WebView view ,
@Nullable Boolean isGeolocationEnabled ) {
view . getSettings (). setGeolocationEnabled ( isGeolocationEnabled != null && isGeolocationEnabled );
}
2019-06-03 03:19:39 +09:00
@ReactProp ( name = "onScroll" )
public void setOnScroll ( WebView view , boolean hasScrollEvent ) {
(( RNCWebView ) view ). setHasScrollEvent ( hasScrollEvent );
}
2018-07-30 20:21:48 -07:00
@Override
protected void addEventEmitters ( ThemedReactContext reactContext , WebView view ) {
// Do not register default touch emitter and let WebView implementation handle touches
2018-09-19 01:19:06 +02:00
view . setWebViewClient ( new RNCWebViewClient ());
2018-07-30 20:21:48 -07:00
}
2018-10-17 21:08:52 +08:00
@Override
public Map getExportedCustomDirectEventTypeConstants () {
2018-11-30 02:59:12 +01:00
Map export = super . getExportedCustomDirectEventTypeConstants ();
if ( export == null ) {
export = MapBuilder . newHashMap ();
}
export . put ( TopLoadingProgressEvent . EVENT_NAME , MapBuilder . of ( "registrationName" , "onLoadingProgress" ));
export . put ( TopShouldStartLoadWithRequestEvent . EVENT_NAME , MapBuilder . of ( "registrationName" , "onShouldStartLoadWithRequest" ));
2019-05-17 15:25:53 +08:00
export . put ( ScrollEventType . getJSEventName ( ScrollEventType . SCROLL ), MapBuilder . of ( "registrationName" , "onScroll" ));
2019-09-20 17:43:28 -07:00
export . put ( TopHttpErrorEvent . EVENT_NAME , MapBuilder . of ( "registrationName" , "onHttpError" ));
2018-11-30 02:59:12 +01:00
return export ;
2018-10-17 21:08:52 +08:00
}
2018-07-30 20:21:48 -07:00
@Override
2019-03-19 15:42:47 +01:00
public @Nullable
Map < String , Integer > getCommandsMap () {
2019-11-12 12:09:16 +03:00
return MapBuilder . < String , Integer > builder ()
. put ( "goBack" , COMMAND_GO_BACK )
. put ( "goForward" , COMMAND_GO_FORWARD )
. put ( "reload" , COMMAND_RELOAD )
. put ( "stopLoading" , COMMAND_STOP_LOADING )
. put ( "postMessage" , COMMAND_POST_MESSAGE )
. put ( "injectJavaScript" , COMMAND_INJECT_JAVASCRIPT )
. put ( "loadUrl" , COMMAND_LOAD_URL )
. put ( "requestFocus" , COMMAND_FOCUS )
. put ( "clearFormData" , COMMAND_CLEAR_FORM_DATA )
. put ( "clearCache" , COMMAND_CLEAR_CACHE )
. put ( "clearHistory" , COMMAND_CLEAR_HISTORY )
. build ();
2018-07-30 20:21:48 -07:00
}
@Override
public void receiveCommand ( WebView root , int commandId , @Nullable ReadableArray args ) {
switch ( commandId ) {
case COMMAND_GO_BACK :
root . goBack ();
break ;
case COMMAND_GO_FORWARD :
root . goForward ();
break ;
case COMMAND_RELOAD :
root . reload ();
break ;
case COMMAND_STOP_LOADING :
root . stopLoading ();
break ;
case COMMAND_POST_MESSAGE :
try {
2018-09-19 01:19:06 +02:00
RNCWebView reactWebView = ( RNCWebView ) root ;
2018-07-30 20:21:48 -07:00
JSONObject eventInitDict = new JSONObject ();
eventInitDict . put ( "data" , args . getString ( 0 ));
2018-09-14 00:36:29 +02:00
reactWebView . evaluateJavascriptWithFallback ( "(function () {" +
2018-07-30 20:21:48 -07:00
"var event;" +
"var data = " + eventInitDict . toString () + ";" +
"try {" +
2019-03-19 15:42:47 +01:00
"event = new MessageEvent('message', data);" +
2018-07-30 20:21:48 -07:00
"} catch (e) {" +
2019-03-19 15:42:47 +01:00
"event = document.createEvent('MessageEvent');" +
"event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" +
2018-07-30 20:21:48 -07:00
"}" +
"document.dispatchEvent(event);" +
2019-03-19 15:42:47 +01:00
"})();" );
2018-07-30 20:21:48 -07:00
} catch ( JSONException e ) {
throw new RuntimeException ( e );
}
break ;
case COMMAND_INJECT_JAVASCRIPT :
2018-09-19 01:19:06 +02:00
RNCWebView reactWebView = ( RNCWebView ) root ;
2018-09-14 00:36:29 +02:00
reactWebView . evaluateJavascriptWithFallback ( args . getString ( 0 ));
2018-07-30 20:21:48 -07:00
break ;
2018-11-30 02:59:12 +01:00
case COMMAND_LOAD_URL :
if ( args == null ) {
throw new RuntimeException ( "Arguments for loading an url are null!" );
}
root . loadUrl ( args . getString ( 0 ));
break ;
2019-08-06 11:56:59 +03:00
case COMMAND_FOCUS :
root . requestFocus ();
break ;
2019-11-12 12:09:16 +03:00
case COMMAND_CLEAR_FORM_DATA :
root . clearFormData ();
break ;
case COMMAND_CLEAR_CACHE :
boolean includeDiskFiles = args != null && args . getBoolean ( 0 );
root . clearCache ( includeDiskFiles );
break ;
case COMMAND_CLEAR_HISTORY :
root . clearHistory ();
break ;
2018-07-30 20:21:48 -07:00
}
}
@Override
public void onDropViewInstance ( WebView webView ) {
super . onDropViewInstance ( webView );
2018-09-19 01:19:06 +02:00
(( ThemedReactContext ) webView . getContext ()). removeLifecycleEventListener (( RNCWebView ) webView );
(( RNCWebView ) webView ). cleanupCallbacksAndDestroy ();
2018-07-30 20:21:48 -07:00
}
2019-05-16 15:22:08 -07:00
public static RNCWebViewModule getModule ( ReactContext reactContext ) {
2019-03-20 14:04:47 +01:00
return reactContext . getNativeModule ( RNCWebViewModule . class );
2018-11-21 12:46:43 +02:00
}
2019-03-19 15:42:47 +01:00
2019-05-16 15:22:08 -07:00
protected void setupWebChromeClient ( ReactContext reactContext , WebView webView ) {
if ( mAllowsFullscreenVideo ) {
2019-08-29 09:38:25 -05:00
int initialRequestedOrientation = reactContext . getCurrentActivity (). getRequestedOrientation ();
2019-05-16 15:22:08 -07:00
mWebChromeClient = new RNCWebChromeClient ( reactContext , webView ) {
2019-11-26 11:40:38 +02:00
@Override
public Bitmap getDefaultVideoPoster () {
return Bitmap . createBitmap ( 50 , 50 , Bitmap . Config . ARGB_8888 );
}
2019-05-16 15:22:08 -07:00
@Override
public void onShowCustomView ( View view , CustomViewCallback callback ) {
if ( mVideoView != null ) {
callback . onCustomViewHidden ();
return ;
}
mVideoView = view ;
mCustomViewCallback = callback ;
2019-08-29 09:38:25 -05:00
mReactContext . getCurrentActivity (). setRequestedOrientation ( ActivityInfo . SCREEN_ORIENTATION_UNSPECIFIED );
2019-05-16 15:22:08 -07:00
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . KITKAT ) {
mVideoView . setSystemUiVisibility ( FULLSCREEN_SYSTEM_UI_VISIBILITY );
mReactContext . getCurrentActivity (). getWindow (). setFlags ( WindowManager . LayoutParams . FLAG_LAYOUT_NO_LIMITS , WindowManager . LayoutParams . FLAG_LAYOUT_NO_LIMITS );
}
mVideoView . setBackgroundColor ( Color . BLACK );
getRootView (). addView ( mVideoView , FULLSCREEN_LAYOUT_PARAMS );
mWebView . setVisibility ( View . GONE );
mReactContext . addLifecycleEventListener ( this );
}
@Override
public void onHideCustomView () {
if ( mVideoView == null ) {
return ;
}
mVideoView . setVisibility ( View . GONE );
getRootView (). removeView ( mVideoView );
mCustomViewCallback . onCustomViewHidden ();
mVideoView = null ;
mCustomViewCallback = null ;
mWebView . setVisibility ( View . VISIBLE );
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . KITKAT ) {
mReactContext . getCurrentActivity (). getWindow (). clearFlags ( WindowManager . LayoutParams . FLAG_LAYOUT_NO_LIMITS );
}
2019-08-29 09:38:25 -05:00
mReactContext . getCurrentActivity (). setRequestedOrientation ( initialRequestedOrientation );
2019-05-16 15:22:08 -07:00
mReactContext . removeLifecycleEventListener ( this );
}
};
webView . setWebChromeClient ( mWebChromeClient );
} else {
if ( mWebChromeClient != null ) {
mWebChromeClient . onHideCustomView ();
}
2019-11-26 11:40:38 +02:00
mWebChromeClient = new RNCWebChromeClient ( reactContext , webView ) {
@Override
public Bitmap getDefaultVideoPoster () {
return Bitmap . createBitmap ( 50 , 50 , Bitmap . Config . ARGB_8888 );
}
};
2019-05-16 15:22:08 -07:00
webView . setWebChromeClient ( mWebChromeClient );
}
}
2020-04-04 16:54:36 +03:00
public static class InputStreamWithInjectedJS extends InputStream {
private InputStream pageIS ;
private InputStream scriptIS ;
private Charset charset ;
private static final String REACT_CLASS = "InputStreamWithInjectedJS" ;
private static Map < Charset , String > script = new HashMap <> ();
2019-03-19 15:42:47 +01:00
2020-04-04 16:54:36 +03:00
private boolean hasJS = false ;
private boolean headWasFound = false ;
private boolean scriptWasInjected = false ;
private StringBuffer contentBuffer = new StringBuffer ();
private static Charset getCharset ( String charsetName ) {
Charset cs = StandardCharsets . UTF_8 ;
try {
if ( charsetName != null ) {
cs = Charset . forName ( charsetName );
}
} catch ( UnsupportedCharsetException e ) {
Log . d ( REACT_CLASS , "wrong charset: " + charsetName );
}
return cs ;
}
private static InputStream getScript ( Charset charset ) {
String js = script . get ( charset );
if ( js == null ) {
String defaultJs = script . get ( StandardCharsets . UTF_8 );
js = new String ( defaultJs . getBytes ( StandardCharsets . UTF_8 ), charset );
script . put ( charset , js );
}
return new ByteArrayInputStream ( js . getBytes ( charset ));
}
InputStreamWithInjectedJS ( InputStream is , String js , Charset charset ) {
if ( js == null ) {
this . pageIS = is ;
} else {
this . hasJS = true ;
this . charset = charset ;
Charset cs = StandardCharsets . UTF_8 ;
String jsScript = "<script>" + js + "</script>" ;
script . put ( cs , jsScript );
this . pageIS = is ;
}
}
@Override
public int read () throws IOException {
if ( scriptWasInjected || ! hasJS ) {
return pageIS . read ();
}
if ( ! scriptWasInjected && headWasFound ) {
int nextByte = scriptIS . read ();
if ( nextByte == - 1 ) {
scriptIS . close ();
scriptWasInjected = true ;
return pageIS . read ();
} else {
return nextByte ;
}
}
if ( ! headWasFound ) {
int nextByte = pageIS . read ();
contentBuffer . append (( char ) nextByte );
int bufferLength = contentBuffer . length ();
if ( nextByte == 62 && bufferLength >= 6 ) {
if ( contentBuffer . substring ( bufferLength - 6 ). equals ( "<head>" )) {
this . scriptIS = getScript ( this . charset );
headWasFound = true ;
}
}
return nextByte ;
}
return pageIS . read ();
}
}
protected class RNCWebViewClient extends WebViewClient {
protected static final String REACT_CLASS = "RNCWebViewClient" ;
2019-03-19 15:42:47 +01:00
protected boolean mLastLoadFailed = false ;
protected @Nullable
ReadableArray mUrlPrefixesForDefaultIntent ;
2020-04-10 13:41:53 -07:00
protected @Nullable String ignoreErrFailedForThisURL = null ;
public void setIgnoreErrFailedForThisURL ( @Nullable String url ) {
ignoreErrFailedForThisURL = url ;
}
2019-03-19 15:42:47 +01:00
2020-04-04 16:54:36 +03:00
2019-03-19 15:42:47 +01:00
@Override
public void onPageFinished ( WebView webView , String url ) {
super . onPageFinished ( webView , url );
if ( ! mLastLoadFailed ) {
RNCWebView reactWebView = ( RNCWebView ) webView ;
reactWebView . callInjectedJavaScript ();
emitFinishEvent ( webView , url );
}
}
@Override
public void onPageStarted ( WebView webView , String url , Bitmap favicon ) {
super . onPageStarted ( webView , url , favicon );
mLastLoadFailed = false ;
dispatchEvent (
webView ,
new TopLoadingStartEvent (
webView . getId (),
createWebViewEvent ( webView , url )));
}
2020-04-04 16:54:36 +03:00
@Override
public WebResourceResponse shouldInterceptRequest ( WebView webView , WebResourceRequest request ) {
Log . d ( REACT_CLASS , "shouldInterceptRequest / WebViewClient" );
WebResourceResponse response = RNCWebViewManager . this . shouldInterceptRequest ( request , true , ( RNCWebView ) webView );
if ( response != null ) {
Log . d ( REACT_CLASS , "shouldInterceptRequest / WebViewClient -> return intercept response" );
return response ;
}
Log . d ( REACT_CLASS , "shouldInterceptRequest / WebViewClient -> intercept response is nil, delegating up" );
return super . shouldInterceptRequest ( webView , request );
}
@Override
public boolean shouldOverrideUrlLoading ( WebView view , WebResourceRequest request ) {
if ( request == null || view == null ) {
return false ;
}
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . N ) {
/*
* In order to follow redirects properly, we return null in interceptRequest().
* Doing this breaks the web3 injection on the resulting page, so we have to reload to
* make sure web3 is available.
* */
if ( request . isForMainFrame () && request . isRedirect ()) {
view . loadUrl ( request . getUrl (). toString ());
return true ;
}
}
/*
* API < 24: TODO: implement based on https://github.com/toshiapp/toshi-android-client/blob/f4840d3d24ff60223662eddddceca8586a1be8bb/app/src/main/java/com/toshi/view/activity/webView/ToshiWebClient.kt#L99
* */
final String url = request . getUrl (). toString ();
return this . shouldOverrideUrlLoading ( view , url );
}
2019-03-19 15:42:47 +01:00
@Override
public boolean shouldOverrideUrlLoading ( WebView view , String url ) {
2019-09-23 01:35:49 -07:00
activeUrl = url ;
2019-03-19 15:42:47 +01:00
dispatchEvent (
view ,
new TopShouldStartLoadWithRequestEvent (
view . getId (),
createWebViewEvent ( view , url )));
2020-01-29 19:18:35 -08:00
return true ;
2019-03-19 15:42:47 +01:00
}
@Override
public void onReceivedError (
WebView webView ,
int errorCode ,
String description ,
String failingUrl ) {
2020-04-10 13:41:53 -07:00
if ( ignoreErrFailedForThisURL != null
&& failingUrl . equals ( ignoreErrFailedForThisURL )
&& errorCode == - 1
&& description . equals ( "net::ERR_FAILED" )) {
// This is a workaround for a bug in the WebView.
// See these chromium issues for more context:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1023678
// https://bugs.chromium.org/p/chromium/issues/detail?id=1050635
// This entire commit should be reverted once this bug is resolved in chromium.
setIgnoreErrFailedForThisURL ( null );
return ;
}
2019-03-19 15:42:47 +01:00
super . onReceivedError ( webView , errorCode , description , failingUrl );
mLastLoadFailed = true ;
// In case of an error JS side expect to get a finish event first, and then get an error event
// Android WebView does it in the opposite way, so we need to simulate that behavior
emitFinishEvent ( webView , failingUrl );
WritableMap eventData = createWebViewEvent ( webView , failingUrl );
eventData . putDouble ( "code" , errorCode );
eventData . putString ( "description" , description );
dispatchEvent (
webView ,
new TopLoadingErrorEvent ( webView . getId (), eventData ));
}
2019-09-20 17:43:28 -07:00
@RequiresApi ( api = Build . VERSION_CODES . M )
@Override
public void onReceivedHttpError (
WebView webView ,
WebResourceRequest request ,
WebResourceResponse errorResponse ) {
super . onReceivedHttpError ( webView , request , errorResponse );
if ( request . isForMainFrame ()) {
WritableMap eventData = createWebViewEvent ( webView , request . getUrl (). toString ());
eventData . putInt ( "statusCode" , errorResponse . getStatusCode ());
eventData . putString ( "description" , errorResponse . getReasonPhrase ());
dispatchEvent (
webView ,
new TopHttpErrorEvent ( webView . getId (), eventData ));
}
}
2019-03-19 15:42:47 +01:00
protected void emitFinishEvent ( WebView webView , String url ) {
dispatchEvent (
webView ,
new TopLoadingFinishEvent (
webView . getId (),
createWebViewEvent ( webView , url )));
}
protected WritableMap createWebViewEvent ( WebView webView , String url ) {
WritableMap event = Arguments . createMap ();
event . putDouble ( "target" , webView . getId ());
// Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks
// like onPageFinished
event . putString ( "url" , url );
event . putBoolean ( "loading" , ! mLastLoadFailed && webView . getProgress () != 100 );
event . putString ( "title" , webView . getTitle ());
event . putBoolean ( "canGoBack" , webView . canGoBack ());
event . putBoolean ( "canGoForward" , webView . canGoForward ());
return event ;
}
public void setUrlPrefixesForDefaultIntent ( ReadableArray specialUrls ) {
mUrlPrefixesForDefaultIntent = specialUrls ;
}
}
2019-05-16 15:22:08 -07:00
protected static class RNCWebChromeClient extends WebChromeClient implements LifecycleEventListener {
protected static final FrameLayout . LayoutParams FULLSCREEN_LAYOUT_PARAMS = new FrameLayout . LayoutParams (
LayoutParams . MATCH_PARENT , LayoutParams . MATCH_PARENT , Gravity . CENTER );
@RequiresApi ( api = Build . VERSION_CODES . KITKAT )
protected static final int FULLSCREEN_SYSTEM_UI_VISIBILITY = View . SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View . SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View . SYSTEM_UI_FLAG_LAYOUT_STABLE |
View . SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View . SYSTEM_UI_FLAG_FULLSCREEN |
View . SYSTEM_UI_FLAG_IMMERSIVE |
View . SYSTEM_UI_FLAG_IMMERSIVE_STICKY ;
protected ReactContext mReactContext ;
protected View mWebView ;
protected View mVideoView ;
protected WebChromeClient . CustomViewCallback mCustomViewCallback ;
public RNCWebChromeClient ( ReactContext reactContext , WebView webView ) {
this . mReactContext = reactContext ;
this . mWebView = webView ;
}
@Override
public boolean onConsoleMessage ( ConsoleMessage message ) {
2020-04-04 16:54:36 +03:00
Log . i ( "StatusNativeLogs" , "###js " + message . message () + " -- From line "
+ message . lineNumber () + " of "
+ message . sourceId ());
2019-05-16 15:22:08 -07:00
if ( ReactBuildConfig . DEBUG ) {
return super . onConsoleMessage ( message );
}
// Ignore console logs in non debug builds.
return true ;
}
2019-08-02 12:23:59 -03:00
// Fix WebRTC permission request error.
2019-10-11 03:45:22 -05:00
@TargetApi ( Build . VERSION_CODES . LOLLIPOP )
2019-08-02 12:23:59 -03:00
@Override
public void onPermissionRequest ( final PermissionRequest request ) {
String [] requestedResources = request . getResources ();
ArrayList < String > permissions = new ArrayList <> ();
ArrayList < String > grantedPermissions = new ArrayList < String > ();
for ( int i = 0 ; i < requestedResources . length ; i ++ ) {
if ( requestedResources [ i ] . equals ( PermissionRequest . RESOURCE_AUDIO_CAPTURE )) {
permissions . add ( Manifest . permission . RECORD_AUDIO );
} else if ( requestedResources [ i ] . equals ( PermissionRequest . RESOURCE_VIDEO_CAPTURE )) {
permissions . add ( Manifest . permission . CAMERA );
}
// TODO: RESOURCE_MIDI_SYSEX, RESOURCE_PROTECTED_MEDIA_ID.
}
for ( int i = 0 ; i < permissions . size (); i ++ ) {
if ( ContextCompat . checkSelfPermission ( mReactContext , permissions . get ( i )) != PackageManager . PERMISSION_GRANTED ) {
continue ;
}
if ( permissions . get ( i ). equals ( Manifest . permission . RECORD_AUDIO )) {
grantedPermissions . add ( PermissionRequest . RESOURCE_AUDIO_CAPTURE );
} else if ( permissions . get ( i ). equals ( Manifest . permission . CAMERA )) {
grantedPermissions . add ( PermissionRequest . RESOURCE_VIDEO_CAPTURE );
}
}
if ( grantedPermissions . isEmpty ()) {
request . deny ();
} else {
String [] grantedPermissionsArray = new String [ grantedPermissions . size () ] ;
grantedPermissionsArray = grantedPermissions . toArray ( grantedPermissionsArray );
request . grant ( grantedPermissionsArray );
}
}
2019-05-16 15:22:08 -07:00
@Override
public void onProgressChanged ( WebView webView , int newProgress ) {
super . onProgressChanged ( webView , newProgress );
2019-09-23 01:35:49 -07:00
final String url = webView . getUrl ();
if (
url != null
&& activeUrl != null
&& ! url . equals ( activeUrl )
) {
return ;
}
2019-05-16 15:22:08 -07:00
WritableMap event = Arguments . createMap ();
event . putDouble ( "target" , webView . getId ());
event . putString ( "title" , webView . getTitle ());
2019-09-23 01:35:49 -07:00
event . putString ( "url" , url );
2019-05-16 15:22:08 -07:00
event . putBoolean ( "canGoBack" , webView . canGoBack ());
event . putBoolean ( "canGoForward" , webView . canGoForward ());
event . putDouble ( "progress" , ( float ) newProgress / 100 );
dispatchEvent (
webView ,
new TopLoadingProgressEvent (
webView . getId (),
event ));
}
@Override
public void onGeolocationPermissionsShowPrompt ( String origin , GeolocationPermissions . Callback callback ) {
callback . invoke ( origin , true , false );
}
protected void openFileChooser ( ValueCallback < Uri > filePathCallback , String acceptType ) {
getModule ( mReactContext ). startPhotoPickerIntent ( filePathCallback , acceptType );
}
protected void openFileChooser ( ValueCallback < Uri > filePathCallback ) {
getModule ( mReactContext ). startPhotoPickerIntent ( filePathCallback , "" );
}
protected void openFileChooser ( ValueCallback < Uri > filePathCallback , String acceptType , String capture ) {
getModule ( mReactContext ). startPhotoPickerIntent ( filePathCallback , acceptType );
}
@TargetApi ( Build . VERSION_CODES . LOLLIPOP )
@Override
public boolean onShowFileChooser ( WebView webView , ValueCallback < Uri []> filePathCallback , FileChooserParams fileChooserParams ) {
String [] acceptTypes = fileChooserParams . getAcceptTypes ();
boolean allowMultiple = fileChooserParams . getMode () == WebChromeClient . FileChooserParams . MODE_OPEN_MULTIPLE ;
Intent intent = fileChooserParams . createIntent ();
return getModule ( mReactContext ). startPhotoPickerIntent ( filePathCallback , intent , acceptTypes , allowMultiple );
}
@Override
public void onHostResume () {
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . KITKAT && mVideoView != null && mVideoView . getSystemUiVisibility () != FULLSCREEN_SYSTEM_UI_VISIBILITY ) {
mVideoView . setSystemUiVisibility ( FULLSCREEN_SYSTEM_UI_VISIBILITY );
}
}
@Override
public void onHostPause () { }
@Override
public void onHostDestroy () { }
protected ViewGroup getRootView () {
return ( ViewGroup ) mReactContext . getCurrentActivity (). findViewById ( android . R . id . content );
}
}
2019-03-19 15:42:47 +01:00
/**
* Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order
* to call {@link WebView#destroy} on activity destroy event and also to clear the client
*/
protected static class RNCWebView extends WebView implements LifecycleEventListener {
protected @Nullable
String injectedJS ;
2020-04-04 16:54:36 +03:00
protected @Nullable String injectedJSBeforeContentLoaded ;
2019-03-19 15:42:47 +01:00
protected boolean messagingEnabled = false ;
protected @Nullable
2020-04-09 18:05:44 +03:00
String messagingModuleName ;
protected @Nullable
2019-03-19 15:42:47 +01:00
RNCWebViewClient mRNCWebViewClient ;
2020-04-06 20:25:28 +03:00
protected @Nullable
CatalystInstance mCatalystInstance ;
2019-03-19 15:42:47 +01:00
protected boolean sendContentSizeChangeEvents = false ;
2019-06-03 03:19:39 +09:00
private OnScrollDispatchHelper mOnScrollDispatchHelper ;
protected boolean hasScrollEvent = false ;
2019-03-19 15:42:47 +01:00
/**
* WebView must be created with an context of the current activity
* <p>
* Activity Context is required for creation of dialogs internally by WebView
* Reactive Native needed for access to ReactNative internal system functionality
*/
public RNCWebView ( ThemedReactContext reactContext ) {
super ( reactContext );
}
2020-04-10 13:41:53 -07:00
public void setIgnoreErrFailedForThisURL ( String url ) {
mRNCWebViewClient . setIgnoreErrFailedForThisURL ( url );
}
2019-03-19 15:42:47 +01:00
public void setSendContentSizeChangeEvents ( boolean sendContentSizeChangeEvents ) {
this . sendContentSizeChangeEvents = sendContentSizeChangeEvents ;
}
2019-06-03 03:19:39 +09:00
public void setHasScrollEvent ( boolean hasScrollEvent ) {
this . hasScrollEvent = hasScrollEvent ;
}
2019-03-19 15:42:47 +01:00
@Override
public void onHostResume () {
// do nothing
}
@Override
public void onHostPause () {
// do nothing
}
@Override
public void onHostDestroy () {
cleanupCallbacksAndDestroy ();
}
@Override
protected void onSizeChanged ( int w , int h , int ow , int oh ) {
super . onSizeChanged ( w , h , ow , oh );
if ( sendContentSizeChangeEvents ) {
dispatchEvent (
this ,
new ContentSizeChangeEvent (
this . getId (),
w ,
h
)
);
}
}
@Override
public void setWebViewClient ( WebViewClient client ) {
super . setWebViewClient ( client );
2019-11-13 02:36:08 -07:00
if ( client instanceof RNCWebViewClient ) {
mRNCWebViewClient = ( RNCWebViewClient ) client ;
}
2019-03-19 15:42:47 +01:00
}
public @Nullable
RNCWebViewClient getRNCWebViewClient () {
return mRNCWebViewClient ;
}
public void setInjectedJavaScript ( @Nullable String js ) {
injectedJS = js ;
}
2020-02-11 15:06:06 +02:00
public void setInjectedJavaScriptBeforeContentLoaded ( @Nullable String js ) {
injectedJSBeforeContentLoaded = js ;
}
2019-03-19 15:42:47 +01:00
protected RNCWebViewBridge createRNCWebViewBridge ( RNCWebView webView ) {
return new RNCWebViewBridge ( webView );
}
2020-04-06 20:25:28 +03:00
protected void createCatalystInstance () {
ReactContext reactContext = ( ReactContext ) this . getContext ();
if ( reactContext != null ) {
mCatalystInstance = reactContext . getCatalystInstance ();
}
}
2019-03-19 15:42:47 +01:00
@SuppressLint ( "AddJavascriptInterface" )
public void setMessagingEnabled ( boolean enabled ) {
if ( messagingEnabled == enabled ) {
return ;
}
messagingEnabled = enabled ;
if ( enabled ) {
addJavascriptInterface ( createRNCWebViewBridge ( this ), JAVASCRIPT_INTERFACE );
2020-04-06 20:25:28 +03:00
this . createCatalystInstance ();
2019-03-19 15:42:47 +01:00
} else {
removeJavascriptInterface ( JAVASCRIPT_INTERFACE );
}
}
2020-04-09 18:05:44 +03:00
public void setMessagingModuleName ( String moduleName ) {
messagingModuleName = moduleName ;
}
2019-03-19 15:42:47 +01:00
protected void evaluateJavascriptWithFallback ( String script ) {
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . KITKAT ) {
evaluateJavascript ( script , null );
return ;
}
try {
loadUrl ( "javascript:" + URLEncoder . encode ( script , "UTF-8" ));
} catch ( UnsupportedEncodingException e ) {
// UTF-8 should always be supported
throw new RuntimeException ( e );
}
}
public void callInjectedJavaScript () {
if ( getSettings (). getJavaScriptEnabled () &&
injectedJS != null &&
! TextUtils . isEmpty ( injectedJS )) {
evaluateJavascriptWithFallback ( "(function() {\n" + injectedJS + ";\n})();" );
2020-02-11 15:06:06 +02:00
//evaluateJavascriptWithFallback(injectedJS);
2019-03-19 15:42:47 +01:00
}
}
public void onMessage ( String message ) {
2020-04-06 20:25:28 +03:00
ReactContext reactContext = ( ReactContext ) this . getContext ();
RNCWebView mContext = this ;
2019-08-02 13:36:11 +05:30
if ( mRNCWebViewClient != null ) {
WebView webView = this ;
webView . post ( new Runnable () {
@Override
public void run () {
2019-08-09 16:53:00 +08:00
if ( mRNCWebViewClient == null ) {
return ;
}
2019-08-02 13:36:11 +05:30
WritableMap data = mRNCWebViewClient . createWebViewEvent ( webView , webView . getUrl ());
data . putString ( "data" , message );
2020-04-06 20:25:28 +03:00
if ( mCatalystInstance != null ) {
mContext . sendDirectMessage ( data );
} else {
dispatchEvent ( webView , new TopMessageEvent ( webView . getId (), data ));
}
2019-08-02 13:36:11 +05:30
}
});
} else {
WritableMap eventData = Arguments . createMap ();
eventData . putString ( "data" , message );
2020-04-06 20:25:28 +03:00
if ( mCatalystInstance != null ) {
this . sendDirectMessage ( eventData );
} else {
dispatchEvent ( this , new TopMessageEvent ( this . getId (), eventData ));
}
2019-08-02 13:36:11 +05:30
}
2019-03-19 15:42:47 +01:00
}
2020-04-06 20:25:28 +03:00
protected void sendDirectMessage ( WritableMap data ) {
WritableNativeMap event = new WritableNativeMap ();
event . putMap ( "nativeEvent" , data );
WritableNativeArray params = new WritableNativeArray ();
params . pushMap ( event );
2020-04-09 18:05:44 +03:00
mCatalystInstance . callFunction ( messagingModuleName , "onMessage" , params );
2020-04-06 20:25:28 +03:00
}
2019-05-16 18:27:16 -04:00
protected void onScrollChanged ( int x , int y , int oldX , int oldY ) {
super . onScrollChanged ( x , y , oldX , oldY );
2019-06-03 03:19:39 +09:00
if ( ! hasScrollEvent ) {
return ;
}
if ( mOnScrollDispatchHelper == null ) {
mOnScrollDispatchHelper = new OnScrollDispatchHelper ();
}
2019-05-16 18:27:16 -04:00
if ( mOnScrollDispatchHelper . onScrollChanged ( x , y )) {
ScrollEvent event = ScrollEvent . obtain (
this . getId (),
ScrollEventType . SCROLL ,
x ,
y ,
mOnScrollDispatchHelper . getXFlingVelocity (),
mOnScrollDispatchHelper . getYFlingVelocity (),
this . computeHorizontalScrollRange (),
this . computeVerticalScrollRange (),
this . getWidth (),
this . getHeight ());
dispatchEvent ( this , event );
}
}
2019-03-19 15:42:47 +01:00
protected void cleanupCallbacksAndDestroy () {
setWebViewClient ( null );
destroy ();
}
protected class RNCWebViewBridge {
RNCWebView mContext ;
RNCWebViewBridge ( RNCWebView c ) {
mContext = c ;
}
/**
* This method is called whenever JavaScript running within the web view calls:
* - window[JAVASCRIPT_INTERFACE].postMessage
*/
@JavascriptInterface
public void postMessage ( String message ) {
mContext . onMessage ( message );
}
}
}
2019-08-29 09:38:25 -05:00
}