feat(android): Add support for injectedJavaScriptBeforeContentLoaded on Android (#1099 by @SRandazzo and @ @shirakaba)

This commit is contained in:
Salvatore Randazzo 2020-06-13 16:54:48 -04:00 committed by GitHub
parent b482bbd3a3
commit ac4e05e0f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 119 additions and 47 deletions

View File

@ -400,6 +400,21 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
((RNCWebView) view).setInjectedJavaScript(injectedJavaScript);
}
@ReactProp(name = "injectedJavaScriptBeforeContentLoaded")
public void setInjectedJavaScriptBeforeContentLoaded(WebView view, @Nullable String injectedJavaScriptBeforeContentLoaded) {
((RNCWebView) view).setInjectedJavaScriptBeforeContentLoaded(injectedJavaScriptBeforeContentLoaded);
}
@ReactProp(name = "injectedJavaScriptForMainFrameOnly")
public void setInjectedJavaScriptForMainFrameOnly(WebView view, boolean enabled) {
((RNCWebView) view).setInjectedJavaScriptForMainFrameOnly(enabled);
}
@ReactProp(name = "injectedJavaScriptBeforeContentLoadedForMainFrameOnly")
public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(WebView view, boolean enabled) {
((RNCWebView) view).setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(enabled);
}
@ReactProp(name = "messagingEnabled")
public void setMessagingEnabled(WebView view, boolean enabled) {
((RNCWebView) view).setMessagingEnabled(enabled);
@ -753,6 +768,9 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
super.onPageStarted(webView, url, favicon);
mLastLoadFailed = false;
RNCWebView reactWebView = (RNCWebView) webView;
reactWebView.callInjectedJavaScriptBeforeContentLoaded();
dispatchEvent(
webView,
new TopLoadingStartEvent(
@ -1011,6 +1029,16 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
protected static class RNCWebView extends WebView implements LifecycleEventListener {
protected @Nullable
String injectedJS;
protected @Nullable
String injectedJSBeforeContentLoaded;
/**
* android.webkit.WebChromeClient fundamentally does not support JS injection into frames other
* than the main frame, so these two properties are mostly here just for parity with iOS & macOS.
*/
protected boolean injectedJavaScriptForMainFrameOnly = true;
protected boolean injectedJavaScriptBeforeContentLoadedForMainFrameOnly = true;
protected boolean messagingEnabled = false;
protected @Nullable
String messagingModuleName;
@ -1105,6 +1133,18 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
injectedJS = js;
}
public void setInjectedJavaScriptBeforeContentLoaded(@Nullable String js) {
injectedJSBeforeContentLoaded = js;
}
public void setInjectedJavaScriptForMainFrameOnly(boolean enabled) {
injectedJavaScriptForMainFrameOnly = enabled;
}
public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(boolean enabled) {
injectedJavaScriptBeforeContentLoadedForMainFrameOnly = enabled;
}
protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
return new RNCWebViewBridge(webView);
}
@ -1159,6 +1199,14 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
}
}
public void callInjectedJavaScriptBeforeContentLoaded() {
if (getSettings().getJavaScriptEnabled() &&
injectedJSBeforeContentLoaded != null &&
!TextUtils.isEmpty(injectedJSBeforeContentLoaded)) {
evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();");
}
}
public void onMessage(String message) {
ReactContext reactContext = (ReactContext) this.getContext();
RNCWebView mContext = this;

View File

@ -88,6 +88,7 @@ class MyWeb extends Component {
}
}
```
</details>
### Controlling navigation state changes
@ -104,14 +105,14 @@ class MyWeb extends Component {
render() {
return (
<WebView
ref={ref => (this.webview = ref)}
ref={(ref) => (this.webview = ref)}
source={{ uri: 'https://reactnative.dev/' }}
onNavigationStateChange={this.handleWebViewNavigationStateChange}
/>
);
}
handleWebViewNavigationStateChange = newNavState => {
handleWebViewNavigationStateChange = (newNavState) => {
// newNavState looks something like this:
// {
// url?: string;
@ -240,11 +241,12 @@ is used to determine if an HTTP response should be a download. On iOS 12 or olde
trigger calls to `onFileDownload`.
Example:
```javascript
onFileDownload = ({ nativeEvent }) => {
const { downloadUrl } = nativeEvent;
// --> Your download code goes here <--
}
onFileDownload = ({ nativeEvent }) => {
const { downloadUrl } = nativeEvent;
// --> Your download code goes here <--
};
```
To be able to save images to the gallery you need to specify this permission in your `ios/[project]/Info.plist` file:
@ -313,7 +315,7 @@ export default class App extends Component {
This runs the JavaScript in the `runFirst` string once the page is loaded. In this case, you can see that both the body style was changed to red and the alert showed up after 2 seconds.
By setting `injectedJavaScriptForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the top frame) if supported for the given platform.
By setting `injectedJavaScriptForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the main frame) if supported for the given platform. For example, if a page contains an iframe, the javascript will be injected into that iframe as well with this set to `false`. (Note this is not supported on Android.) There is also `injectedJavaScriptBeforeContentLoadedForMainFrameOnly` for injecting prior to content loading. Read more about this in the [Reference](./Reference.md#injectedjavascriptformainframeonly).
<img alt="screenshot of Github repo" width="200" src="https://user-images.githubusercontent.com/1479215/53609254-e5dc9c00-3b7a-11e9-9118-bc4e520ce6ca.png" />
@ -354,10 +356,11 @@ export default class App extends Component {
This runs the JavaScript in the `runFirst` string before the page is loaded. In this case, the value of `window.isNativeApp` will be set to true before the web code executes.
By setting `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the top frame) if supported for the given platform. Howver, although support for `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false` has been implemented for iOS and macOS, [it is not clear](https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-600275750) that it is actually possible to inject JS into iframes at this point in the page lifecycle, and so relying on the expected behaviour of this prop when set to `false` is not recommended.
By setting `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the top frame) if supported for the given platform. However, although support for `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false` has been implemented for iOS and macOS, [it is not clear](https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-600275750) that it is actually possible to inject JS into iframes at this point in the page lifecycle, and so relying on the expected behaviour of this prop when set to `false` is not recommended.
> On iOS, ~~`injectedJavaScriptBeforeContentLoaded` runs a method on WebView called `evaluateJavaScript:completionHandler:`~~ this is no longer true as of version `8.2.0`. Instead, we use a `WKUserScript` with injection time `WKUserScriptInjectionTimeAtDocumentStart`. As a consequence, `injectedJavaScriptBeforeContentLoaded` no longer returns an evaluation value nor logs a warning to the console. In the unlikely event that your app depended upon this behaviour, please see migration steps [here](https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-574919464) to retain equivalent behaviour.
> On Android, `injectedJavaScript` runs a method on the Android WebView called `evaluateJavascriptWithFallback`
> Note on Android Compatibility: For applications targeting `Build.VERSION_CODES.N` or later, JavaScript state from an empty WebView is no longer persisted across navigations like `loadUrl(java.lang.String)`. For example, global variables and functions defined before calling `loadUrl(java.lang.String)` will not exist in the loaded page. Applications should use the Android Native API `addJavascriptInterface(Object, String)` instead to persist JavaScript objects across navigations.
#### The `injectJavaScript` method
@ -382,7 +385,7 @@ export default class App extends Component {
return (
<View style={{ flex: 1 }}>
<WebView
ref={r => (this.webref = r)}
ref={(r) => (this.webref = r)}
source={{
uri:
'https://github.com/react-native-community/react-native-webview',
@ -435,7 +438,7 @@ export default class App extends Component {
<View style={{ flex: 1 }}>
<WebView
source={{ html }}
onMessage={event => {
onMessage={(event) => {
alert(event.nativeEvent.data);
}}
/>
@ -471,7 +474,7 @@ This will set the header on the first load, but not on subsequent page navigatio
In order to work around this, you can track the current URL, intercept new page loads, and navigate to them yourself ([original credit for this technique to Chirag Shah from Big Binary](https://blog.bigbinary.com/2016/07/26/passing-request-headers-on-each-webview-request-in-react-native.html)):
```jsx
const CustomHeaderWebView = props => {
const CustomHeaderWebView = (props) => {
const { uri, onLoadStart, ...restProps } = props;
const [currentURI, setURI] = useState(props.source.uri);
const newSource = { ...props.source, uri: currentURI };
@ -480,7 +483,7 @@ const CustomHeaderWebView = props => {
<WebView
{...restProps}
source={newSource}
onShouldStartLoadWithRequest={request => {
onShouldStartLoadWithRequest={(request) => {
// If we're loading the current URI, allow it to load
if (request.url === currentURI) return true;
// We're loading a new URL -- change state first
@ -539,6 +542,7 @@ const App = () => {
Note that these cookies will only be sent on the first request unless you use the technique above for [setting custom headers on each page load](#Setting-Custom-Headers).
### Hardware Silence Switch
There are some inconsistencies in how the hardware silence switch is handled between embedded `audio` and `video` elements and between iOS and Android platforms.
Audio on `iOS` will be muted when the hardware silence switch is in the on position, unless the `ignoreSilentHardwareSwitch` parameter is set to true.

View File

@ -190,27 +190,25 @@ const INJECTED_JAVASCRIPT = `(function() {
### `injectedJavaScriptForMainFrameOnly`
If `true` (default), loads the `injectedJavaScript` only into the main frame.
If `true` (default; mandatory for Android), loads the `injectedJavaScript` only into the main frame.
If `false`, loads it into all frames (e.g. iframes).
If `false`, (only supported on iOS and macOS), loads it into all frames (e.g. iframes).
| Type | Required | Platform |
| ------ | -------- | -------- |
| bool | No | iOS, macOS |
| bool | No | iOS and macOS (only `true` supported for Android) |
---
### `injectedJavaScriptBeforeContentLoadedForMainFrameOnly`
If `true` (default), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame.
If `true` (default; mandatory for Android), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame.
If `false`, loads it into all frames (e.g. iframes).
Warning: although support for `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false` has been implemented for iOS and macOS, [it is not clear](https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-600275750) that it is actually possible to inject JS into iframes at this point in the page lifecycle, and so relying on the expected behaviour of this prop when set to `false` is not recommended.
If `false`, (only supported on iOS and macOS), loads it into all frames (e.g. iframes).
| Type | Required | Platform |
| ------ | -------- | -------- |
| bool | No | iOS, macOS |
| bool | No | iOS and macOS (only `true` supported for Android) |
---

View File

@ -2,6 +2,9 @@ package com.example;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.webkit.WebView;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
@ -44,6 +47,10 @@ public class MainApplication extends Application implements ReactApplication {
@Override
public void onCreate() {
super.onCreate();
/* https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this); // Remove this line if you don't want Flipper enabled
}

View File

@ -3,23 +3,23 @@ import {Text, View, ScrollView} from 'react-native';
import WebView from 'react-native-webview';
// const HTML = `
// <!DOCTYPE html>
// <html>
// <head>
// <meta charset="utf-8">
// <meta name="viewport" content="width=device-width, initial-scale=1">
// <title>iframe test</title>
// </head>
// <body>
// <p style="">beforeContentLoaded on the top frame <span id="before_failed" style="display: inline-block;">failed</span><span id="before_succeeded" style="display: none;">succeeded</span>!</p>
// <p style="">afterContentLoaded on the top frame <span id="after_failed" style="display: inline-block;">failed</span><span id="after_succeeded" style="display: none;">succeeded</span>!</p>
// <iframe src="https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframe.html?v=1" name="iframe_0" style="width: 100%; height: 25px;"></iframe>
// <iframe src="https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframe2.html?v=1" name="iframe_1" style="width: 100%; height: 25px;"></iframe>
// <iframe src="https://www.ebay.co.uk" name="iframe_2" style="width: 100%; height: 25px;"></iframe>
// </body>
// </html>
// `;
const HTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>iframe test</title>
</head>
<body>
<p style="">beforeContentLoaded on the top frame <span id="before_failed" style="display: inline-block;">failed</span><span id="before_succeeded" style="display: none;">succeeded</span>!</p>
<p style="">afterContentLoaded on the top frame <span id="after_failed" style="display: inline-block;">failed</span><span id="after_succeeded" style="display: none;">succeeded</span>!</p>
<iframe src="https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframe.html?v=1" name="iframe_0" style="width: 100%; height: 25px;"></iframe>
<iframe src="https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframe2.html?v=1" name="iframe_1" style="width: 100%; height: 25px;"></iframe>
<iframe src="https://www.ebay.co.uk" name="iframe_2" style="width: 100%; height: 25px;"></iframe>
</body>
</html>
`;
type Props = {};
type State = {
@ -35,11 +35,12 @@ export default class Injection extends Component<Props, State> {
return (
<ScrollView>
<View style={{ }}>
<View style={{ height: 300 }}>
<View style={{ height: 400 }}>
<WebView
/**
* This HTML is a copy of a multi-frame JS injection test that I had lying around.
* @see https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframeTest.html
* This HTML is a copy of the hosted multi-frame JS injection test.
* I have found that Android doesn't support beforeContentLoaded for a hosted HTML webpage, yet does for a static source.
* The cause of this is unresolved.
*/
// source={{ html: HTML }}
source={{ uri: "https://birchlabs.co.uk/linguabrowse/infopages/obsol/rnw_iframe_test.html" }}
@ -50,10 +51,12 @@ export default class Injection extends Component<Props, State> {
* JS injection user scripts, consistent with current behaviour. This is undesirable,
* so needs addressing in a follow-up PR. */
onMessage={() => {}}
injectedJavaScriptBeforeContentLoadedForMainFrameOnly={false}
injectedJavaScriptForMainFrameOnly={false}
/* We set this property in each frame */
injectedJavaScriptBeforeContentLoaded={`
console.log("executing injectedJavaScriptBeforeContentLoaded...");
console.log("executing injectedJavaScriptBeforeContentLoaded... " + (new Date()).toString());
if(typeof window.top.injectedIframesBeforeContentLoaded === "undefined"){
window.top.injectedIframesBeforeContentLoaded = [];
}
@ -84,12 +87,10 @@ export default class Injection extends Component<Props, State> {
console.log("wasn't window.top. Still going...");
}
`}
injectedJavaScriptForMainFrameOnly={false}
/* We read the colourToUse property in each frame to recolour each frame */
injectedJavaScript={`
console.log("executing injectedJavaScript...");
console.log("executing injectedJavaScript... " + (new Date()).toString());
if(typeof window.top.injectedIframesAfterContentLoaded === "undefined"){
window.top.injectedIframesAfterContentLoaded = [];
}
@ -119,7 +120,7 @@ export default class Injection extends Component<Props, State> {
// numberOfFramesAtAfterContentLoadedEle.id = "numberOfFramesAtAfterContentLoadedEle";
var namedFramesAtBeforeContentLoadedEle = document.createElement('p');
namedFramesAtBeforeContentLoadedEle.textContent = "Names of iframes that called beforeContentLoaded: " + JSON.stringify(window.top.injectedIframesBeforeContentLoaded);
namedFramesAtBeforeContentLoadedEle.textContent = "Names of iframes that called beforeContentLoaded: " + JSON.stringify(window.top.injectedIframesBeforeContentLoaded || []);
namedFramesAtBeforeContentLoadedEle.id = "namedFramesAtBeforeContentLoadedEle";
var namedFramesAtAfterContentLoadedEle = document.createElement('p');
@ -147,8 +148,8 @@ export default class Injection extends Component<Props, State> {
<Text> If the main frame becomes orange, then top-frame injection both beforeContentLoaded and afterContentLoaded is supported.</Text>
<Text> If iframe_0, and iframe_1 become orange, then multi-frame injection beforeContentLoaded and afterContentLoaded is supported.</Text>
<Text> If the two texts say "beforeContentLoaded on the top frame succeeded!" and "afterContentLoaded on the top frame succeeded!", then both injection times are supported at least on the main frame.</Text>
<Text> If either of the two iframes become coloured cyan, then for that given frame, JS injection succeeded after the content loaded, but didn't occur before the content loaded - please note that for iframes, this may not be a test failure, as it is not clear whether we would expect iframes to support an injection time of beforeContentLoaded anyway.</Text>
<Text> If "Names of iframes that called beforeContentLoaded: " is [], then see above.</Text>
<Text> If either of the two iframes become coloured cyan, then for that given frame, JS injection succeeded after the content loaded, but didn't occur before the content loaded.</Text>
<Text> If "Names of iframes that called beforeContentLoaded: " is [], then see above.</Text>
<Text> If "Names of iframes that called afterContentLoaded: " is [], then afterContentLoaded is not supported in iframes.</Text>
<Text> If the main frame becomes coloured cyan, then JS injection succeeded after the content loaded, but didn't occur before the content loaded.</Text>
<Text> If the text "beforeContentLoaded on the top frame failed" remains unchanged, then JS injection has failed on the main frame before the content loaded.</Text>

View File

@ -240,6 +240,8 @@ export interface CommonNativeWebViewProps extends ViewProps {
incognito?: boolean;
injectedJavaScript?: string;
injectedJavaScriptBeforeContentLoaded?: string;
injectedJavaScriptForMainFrameOnly?: boolean;
injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
javaScriptCanOpenWindowsAutomatically?: boolean;
mediaPlaybackRequiresUserAction?: boolean;
messagingEnabled: boolean;
@ -915,6 +917,18 @@ export interface WebViewSharedProps extends ViewProps {
*/
injectedJavaScriptBeforeContentLoaded?: string;
/**
* If `true` (default; mandatory for Android), loads the `injectedJavaScript` only into the main frame.
* If `false` (only supported on iOS and macOS), loads it into all frames (e.g. iframes).
*/
injectedJavaScriptForMainFrameOnly?: boolean;
/**
* If `true` (default; mandatory for Android), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame.
* If `false` (only supported on iOS and macOS), loads it into all frames (e.g. iframes).
*/
injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
/**
* Boolean value that determines whether a horizontal scroll indicator is
* shown in the `WebView`. The default value is `true`.