Skip to content

Commit d3e128e

Browse files
committed
first attempt to bring WebViewBridge to android
1 parent 6d996ec commit d3e128e

File tree

6 files changed

+507
-0
lines changed

6 files changed

+507
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="com.github.alinz.reactnativewebviewbridge">
3+
4+
</manifest>
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
package com.github.alinz.reactnativewebviewbridge;
11+
12+
import javax.annotation.Nullable;
13+
14+
import java.util.Map;
15+
16+
import android.graphics.Bitmap;
17+
import android.os.Build;
18+
import android.os.SystemClock;
19+
import android.text.TextUtils;
20+
import android.webkit.WebView;
21+
import android.webkit.WebViewClient;
22+
23+
import com.github.alinz.reactnativewebviewbridge.events.TopLoadingErrorEvent;
24+
import com.github.alinz.reactnativewebviewbridge.events.TopLoadingFinishEvent;
25+
import com.github.alinz.reactnativewebviewbridge.events.TopLoadingStartEvent;
26+
import com.facebook.react.bridge.Arguments;
27+
import com.facebook.react.bridge.LifecycleEventListener;
28+
import com.facebook.react.bridge.ReactContext;
29+
import com.facebook.react.bridge.ReadableArray;
30+
import com.facebook.react.bridge.WritableMap;
31+
import com.facebook.react.common.MapBuilder;
32+
import com.facebook.react.common.build.ReactBuildConfig;
33+
import com.facebook.react.uimanager.annotations.ReactProp;
34+
import com.facebook.react.uimanager.SimpleViewManager;
35+
import com.facebook.react.uimanager.ThemedReactContext;
36+
import com.facebook.react.uimanager.UIManagerModule;
37+
import com.facebook.react.uimanager.events.Event;
38+
import com.facebook.react.uimanager.events.EventDispatcher;
39+
40+
/**
41+
* Manages instances of {@link WebView}
42+
*
43+
* Can accept following commands:
44+
* - GO_BACK
45+
* - GO_FORWARD
46+
* - RELOAD
47+
*
48+
* {@link WebView} instances could emit following direct events:
49+
* - topLoadingFinish
50+
* - topLoadingStart
51+
* - topLoadingError
52+
*
53+
* Each event will carry the following properties:
54+
* - target - view's react tag
55+
* - url - url set for the webview
56+
* - loading - whether webview is in a loading state
57+
* - title - title of the current page
58+
* - canGoBack - boolean, whether there is anything on a history stack to go back
59+
* - canGoForward - boolean, whether it is possible to request GO_FORWARD command
60+
*/
61+
public class ReactWebViewBridgeManager extends SimpleViewManager<WebView> {
62+
63+
private static final String REACT_CLASS = "RCTWebView";
64+
65+
private static final String HTML_ENCODING = "UTF-8";
66+
private static final String HTML_MIME_TYPE = "text/html; charset=utf-8";
67+
68+
public static final int COMMAND_GO_BACK = 1;
69+
public static final int COMMAND_GO_FORWARD = 2;
70+
public static final int COMMAND_RELOAD = 3;
71+
72+
// Use `webView.loadUrl("about:blank")` to reliably reset the view
73+
// state and release page resources (including any running JavaScript).
74+
private static final String BLANK_URL = "about:blank";
75+
76+
private WebViewConfig mWebViewConfig;
77+
78+
private static class ReactWebViewClient extends WebViewClient {
79+
80+
private boolean mLastLoadFailed = false;
81+
82+
@Override
83+
public void onPageFinished(WebView webView, String url) {
84+
super.onPageFinished(webView, url);
85+
86+
if (!mLastLoadFailed) {
87+
ReactWebView reactWebView = (ReactWebView) webView;
88+
reactWebView.callInjectedJavaScript();
89+
emitFinishEvent(webView, url);
90+
}
91+
}
92+
93+
@Override
94+
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
95+
super.onPageStarted(webView, url, favicon);
96+
mLastLoadFailed = false;
97+
98+
dispatchEvent(
99+
webView,
100+
new TopLoadingStartEvent(
101+
webView.getId(),
102+
SystemClock.uptimeMillis(),
103+
createWebViewEvent(webView, url)));
104+
}
105+
106+
@Override
107+
public void onReceivedError(
108+
WebView webView,
109+
int errorCode,
110+
String description,
111+
String failingUrl) {
112+
super.onReceivedError(webView, errorCode, description, failingUrl);
113+
mLastLoadFailed = true;
114+
115+
// In case of an error JS side expect to get a finish event first, and then get an error event
116+
// Android WebView does it in the opposite way, so we need to simulate that behavior
117+
emitFinishEvent(webView, failingUrl);
118+
119+
WritableMap eventData = createWebViewEvent(webView, failingUrl);
120+
eventData.putDouble("code", errorCode);
121+
eventData.putString("description", description);
122+
123+
dispatchEvent(
124+
webView,
125+
new TopLoadingErrorEvent(webView.getId(), SystemClock.uptimeMillis(), eventData));
126+
}
127+
128+
@Override
129+
public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) {
130+
super.doUpdateVisitedHistory(webView, url, isReload);
131+
132+
dispatchEvent(
133+
webView,
134+
new TopLoadingStartEvent(
135+
webView.getId(),
136+
SystemClock.uptimeMillis(),
137+
createWebViewEvent(webView, url)));
138+
}
139+
140+
private void emitFinishEvent(WebView webView, String url) {
141+
dispatchEvent(
142+
webView,
143+
new TopLoadingFinishEvent(
144+
webView.getId(),
145+
SystemClock.uptimeMillis(),
146+
createWebViewEvent(webView, url)));
147+
}
148+
149+
private static void dispatchEvent(WebView webView, Event event) {
150+
ReactContext reactContext = (ReactContext) webView.getContext();
151+
EventDispatcher eventDispatcher =
152+
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
153+
eventDispatcher.dispatchEvent(event);
154+
}
155+
156+
private WritableMap createWebViewEvent(WebView webView, String url) {
157+
WritableMap event = Arguments.createMap();
158+
event.putDouble("target", webView.getId());
159+
// Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks
160+
// like onPageFinished
161+
event.putString("url", url);
162+
event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100);
163+
event.putString("title", webView.getTitle());
164+
event.putBoolean("canGoBack", webView.canGoBack());
165+
event.putBoolean("canGoForward", webView.canGoForward());
166+
return event;
167+
}
168+
}
169+
170+
/**
171+
* Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order
172+
* to call {@link WebView#destroy} on activty destroy event and also to clear the client
173+
*/
174+
private static class ReactWebView extends WebView implements LifecycleEventListener {
175+
private @Nullable String injectedJS;
176+
177+
/**
178+
* WebView must be created with an context of the current activity
179+
*
180+
* Activity Context is required for creation of dialogs internally by WebView
181+
* Reactive Native needed for access to ReactNative internal system functionality
182+
*
183+
*/
184+
public ReactWebView(ThemedReactContext reactContext) {
185+
super(reactContext);
186+
}
187+
188+
@Override
189+
public void onHostResume() {
190+
// do nothing
191+
}
192+
193+
@Override
194+
public void onHostPause() {
195+
// do nothing
196+
}
197+
198+
@Override
199+
public void onHostDestroy() {
200+
cleanupCallbacksAndDestroy();
201+
}
202+
203+
public void setInjectedJavaScript(@Nullable String js) {
204+
injectedJS = js;
205+
}
206+
207+
public void callInjectedJavaScript() {
208+
if (
209+
getSettings().getJavaScriptEnabled() &&
210+
injectedJS != null &&
211+
!TextUtils.isEmpty(injectedJS)) {
212+
loadUrl("javascript:(function() {\n" + injectedJS + ";\n})();");
213+
}
214+
}
215+
216+
private void cleanupCallbacksAndDestroy() {
217+
setWebViewClient(null);
218+
destroy();
219+
}
220+
}
221+
222+
public ReactWebViewBridgeManager() {
223+
mWebViewConfig = new WebViewConfig() {
224+
public void configWebView(WebView webView) {
225+
}
226+
};
227+
}
228+
229+
public ReactWebViewBridgeManager(WebViewConfig webViewConfig) {
230+
mWebViewConfig = webViewConfig;
231+
}
232+
233+
@Override
234+
public String getName() {
235+
return REACT_CLASS;
236+
}
237+
238+
@Override
239+
protected WebView createViewInstance(ThemedReactContext reactContext) {
240+
ReactWebView webView = new ReactWebView(reactContext);
241+
reactContext.addLifecycleEventListener(webView);
242+
mWebViewConfig.configWebView(webView);
243+
244+
if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
245+
WebView.setWebContentsDebuggingEnabled(true);
246+
}
247+
248+
return webView;
249+
}
250+
251+
@ReactProp(name = "javaScriptEnabled")
252+
public void setJavaScriptEnabled(WebView view, boolean enabled) {
253+
view.getSettings().setJavaScriptEnabled(enabled);
254+
}
255+
256+
@ReactProp(name = "domStorageEnabled")
257+
public void setDomStorageEnabled(WebView view, boolean enabled) {
258+
view.getSettings().setDomStorageEnabled(enabled);
259+
}
260+
261+
@ReactProp(name = "userAgent")
262+
public void setUserAgent(WebView view, @Nullable String userAgent) {
263+
if (userAgent != null) {
264+
// TODO(8496850): Fix incorrect behavior when property is unset (uA == null)
265+
view.getSettings().setUserAgentString(userAgent);
266+
}
267+
}
268+
269+
@ReactProp(name = "injectedJavaScript")
270+
public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScript) {
271+
((ReactWebView) view).setInjectedJavaScript(injectedJavaScript);
272+
}
273+
274+
@ReactProp(name = "html")
275+
public void setHtml(WebView view, @Nullable String html) {
276+
if (html != null) {
277+
view.loadData(html, HTML_MIME_TYPE, HTML_ENCODING);
278+
} else {
279+
view.loadUrl(BLANK_URL);
280+
}
281+
}
282+
283+
@ReactProp(name = "url")
284+
public void setUrl(WebView view, @Nullable String url) {
285+
// TODO(8495359): url and html are coupled as they both call loadUrl, therefore in case when
286+
// property url is removed in favor of property html being added in single transaction we may
287+
// end up in a state when blank url is loaded as it depends on the order of update operations!
288+
289+
String currentUrl = view.getUrl();
290+
if (currentUrl != null && currentUrl.equals(url)) {
291+
// We are already loading it so no need to stomp over it and start again
292+
return;
293+
}
294+
if (url != null) {
295+
view.loadUrl(url);
296+
} else {
297+
view.loadUrl(BLANK_URL);
298+
}
299+
}
300+
301+
@Override
302+
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
303+
// Do not register default touch emitter and let WebView implementation handle touches
304+
view.setWebViewClient(new ReactWebViewClient());
305+
}
306+
307+
@Override
308+
public @Nullable Map<String, Integer> getCommandsMap() {
309+
return MapBuilder.of(
310+
"goBack", COMMAND_GO_BACK,
311+
"goForward", COMMAND_GO_FORWARD,
312+
"reload", COMMAND_RELOAD);
313+
}
314+
315+
@Override
316+
public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray args) {
317+
switch (commandId) {
318+
case COMMAND_GO_BACK:
319+
root.goBack();
320+
break;
321+
case COMMAND_GO_FORWARD:
322+
root.goForward();
323+
break;
324+
case COMMAND_RELOAD:
325+
root.reload();
326+
break;
327+
}
328+
}
329+
330+
@Override
331+
public void onDropViewInstance(WebView webView) {
332+
super.onDropViewInstance(webView);
333+
((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((ReactWebView) webView);
334+
((ReactWebView) webView).cleanupCallbacksAndDestroy();
335+
}
336+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
package com.github.alinz.reactnativewebviewbridge;
11+
12+
import android.webkit.WebView;
13+
14+
/**
15+
* Implement this interface in order to config your {@link WebView}. An instance of that
16+
* implementation will have to be given as a constructor argument to {@link ReactWebViewManager}.
17+
*/
18+
public interface WebViewConfig {
19+
void configWebView(WebView webView);
20+
}

0 commit comments

Comments
 (0)