Skip to content

Commit

Permalink
feat: android should intercept request
Browse files Browse the repository at this point in the history
  • Loading branch information
AbrahamOsmondE committed Apr 30, 2024
1 parent 7119160 commit 4807f2f
Show file tree
Hide file tree
Showing 12 changed files with 327 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,14 @@ protected boolean dispatchDirectShouldStartLoadWithRequest(WritableMap data) {
return true;
}

protected boolean dispatchDirectShouldInterceptRequest(WritableMap data) {
WritableNativeMap event = new WritableNativeMap();
event.putMap("nativeEvent", data);
event.putString("messagingModuleName", messagingModuleName);
mMessagingJSModule.onShouldInterceptRequest(event);
return true;
}

protected void onScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent;
import com.reactnativecommunity.webview.events.TopShouldInterceptRequestEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;

public class RNCWebViewClient extends WebViewClient {
private static String TAG = "RNCWebViewClient";
protected static final int SHOULD_OVERRIDE_URL_LOADING_TIMEOUT = 250;

protected static final int SHOULD_INTERCEPT_REQUEST_LOADING_TIMEOUT = 1000;
protected boolean mLastLoadFailed = false;
protected RNCWebView.ProgressChangedFilter progressChangedFilter = null;
protected @Nullable String ignoreErrFailedForThisURL = null;
Expand Down Expand Up @@ -84,7 +87,72 @@ public void onPageStarted(WebView webView, String url, Bitmap favicon) {
reactWebView.callInjectedJavaScriptBeforeContentLoaded();
}

@Override
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
final RNCWebView rncWebView = (RNCWebView) view;
final boolean isJsDebugging = rncWebView.getReactApplicationContext().getJavaScriptContextHolder().get() == 0;
final String url = request.getUrl().toString();
FLog.w(TAG, "Request Webview Class: " + request.getUrl() + request.getRequestHeaders() + request.getClass() + request.isForMainFrame() + request.isRedirect() + request.getMethod());
if (!isJsDebugging && rncWebView.mMessagingJSModule != null) {
final Pair<Double, AtomicReference<String>> lock = RNCWebViewModuleImpl.shouldInterceptRequestLoadingLock.getNewLock();
final double lockIdentifier = lock.first;
final AtomicReference<String> lockObject = lock.second;

final WritableMap event = createWebViewEventTest(view, url);
event.putDouble("lockIdentifier", lockIdentifier);
rncWebView.dispatchDirectShouldInterceptRequest(event);

try {
assert lockObject != null;
synchronized (lockObject) {
final long startTime = SystemClock.elapsedRealtime();
while (lockObject.get() == null) {
if (SystemClock.elapsedRealtime() - startTime > SHOULD_INTERCEPT_REQUEST_LOADING_TIMEOUT) {
FLog.w(TAG, "Did not receive response to shouldInterceptRequest in time, defaulting to allow loading.");
RNCWebViewModuleImpl.shouldInterceptRequestLoadingLock.removeLock(lockIdentifier);
return super.shouldInterceptRequest(view, request);
}
lockObject.wait(SHOULD_INTERCEPT_REQUEST_LOADING_TIMEOUT);
}
}
} catch (InterruptedException e) {
FLog.e(TAG, "shouldInterceptRequest was interrupted while waiting for result.", e);

RNCWebViewModuleImpl.shouldInterceptRequestLoadingLock.removeLock(lockIdentifier);
return super.shouldInterceptRequest(view, request);
}

final String input = lockObject.get();
final boolean shouldIntercept = !input.isEmpty();

RNCWebViewModuleImpl.shouldInterceptRequestLoadingLock.removeLock(lockIdentifier);

if (shouldIntercept) {
byte[] byteArrayInput = input.getBytes(StandardCharsets.UTF_8);
return new WebResourceResponse(
"text/html",
"utf-8",
200,
"OK",
request.getRequestHeaders(),
new ByteArrayInputStream(byteArrayInput)
);
} else {
return super.shouldInterceptRequest(view, request);
}
} else {
FLog.w(TAG, "Couldn't use blocking synchronous call for shouldInterceptRequest due to debugging or missing Catalyst instance, falling back to old event-and-load.");
progressChangedFilter.setWaitingForCommandLoadUrl(true);

int reactTag = RNCWebViewWrapper.getReactTagFromWebView(view);
UIManagerHelper.getEventDispatcherForReactTag((ReactContext) view.getContext(), reactTag).dispatchEvent(new TopShouldInterceptRequestEvent(
reactTag,
createWebViewEvent(view, url)));
return super.shouldInterceptRequest(view, request);
}
}

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
final RNCWebView rncWebView = (RNCWebView) view;
final boolean isJsDebugging = rncWebView.getReactApplicationContext().getJavaScriptContextHolder().get() == 0;
Expand Down Expand Up @@ -312,6 +380,19 @@ protected WritableMap createWebViewEvent(WebView webView, String url) {
return event;
}

protected WritableMap createWebViewEventTest(WebView webView, String url) {
WritableMap event = Arguments.createMap();
event.putDouble("target", RNCWebViewWrapper.getReactTagFromWebView(webView));
// 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", false);
event.putString("title", "title");
event.putBoolean("canGoBack", true);
event.putBoolean("canGoForward", true);
return event;
}

public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) {
progressChangedFilter = filter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import com.facebook.react.bridge.WritableMap
internal interface RNCWebViewMessagingModule : JavaScriptModule {
fun onShouldStartLoadWithRequest(event: WritableMap)
fun onMessage(event: WritableMap)
fun onShouldInterceptRequest(event: WritableMap)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import android.widget.Toast;

import com.facebook.common.activitylistener.ActivityListenerManager;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
Expand Down Expand Up @@ -163,6 +164,35 @@ public synchronized void removeLock(Double lockIdentifier) {

protected static final ShouldOverrideUrlLoadingLock shouldOverrideUrlLoadingLock = new ShouldOverrideUrlLoadingLock();

protected static class ShouldInterceptRequestLock {
protected enum ShouldInterceptRequestState {
UNDECIDED,
SHOULD_INTERCEPT,
DO_NOT_INTERCEPT,
}

private double nextLockIdentifier = 1;
private final HashMap<Double, AtomicReference<String>> shouldInterceptLocks = new HashMap<>();

public synchronized Pair<Double, AtomicReference<String>> getNewLock() {
final double lockIdentifier = nextLockIdentifier++;

final AtomicReference<String> shouldIntercept = new AtomicReference<>(null);
shouldInterceptLocks.put(lockIdentifier, shouldIntercept);
return new Pair<>(lockIdentifier, shouldIntercept);
}

@Nullable
public synchronized AtomicReference<String> getLock(Double lockIdentifier) {
return shouldInterceptLocks.get(lockIdentifier);
}

public synchronized void removeLock(Double lockIdentifier) {
shouldInterceptLocks.remove(lockIdentifier);
}
}

protected static final ShouldInterceptRequestLock shouldInterceptRequestLoadingLock = new ShouldInterceptRequestLock();
private enum MimeType {
DEFAULT("*/*"),
IMAGE("image"),
Expand Down Expand Up @@ -211,6 +241,18 @@ public void shouldStartLoadWithLockIdentifier(boolean shouldStart, double lockId
}
}

public void shouldInterceptRequestLockIdentifier(boolean shouldIntercept, double lockIdentifier, String input) {
final AtomicReference<String> lockObject = shouldInterceptRequestLoadingLock.getLock(lockIdentifier);
FLog.w("TAG", "Should Intercept Received from React Native: " + input + shouldIntercept);

if (lockObject != null) {
synchronized (lockObject) {
lockObject.set(shouldIntercept ? input : "");
lockObject.notify();
}
}
}

public Uri[] getSelectedFiles(Intent data, int resultCode) {
if (data == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.reactnativecommunity.webview.events

import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter

/**
* Event emitted when shouldInterceptRequest is called
*/
class TopShouldInterceptRequestEvent(viewId: Int, private val mData: WritableMap) : Event<TopShouldInterceptRequestEvent>(viewId) {
companion object {
const val EVENT_NAME = "topShouldInterceptRequest"
}

init {
mData.putString("navigationType", "other")
}

override fun getEventName(): String = EVENT_NAME

override fun canCoalesce(): Boolean = false

override fun getCoalescingKey(): Short = 0

override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, EVENT_NAME, mData)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public void shouldStartLoadWithLockIdentifier(boolean shouldStart, double lockId
mRNCWebViewModuleImpl.shouldStartLoadWithLockIdentifier(shouldStart, lockIdentifier);
}

@ReactMethod
public void shouldInterceptRequestLockIdentifier(boolean shouldIntercept, double lockIdentifier, String input) {
mRNCWebViewModuleImpl.shouldInterceptRequestLockIdentifier(shouldIntercept, lockIdentifier, input);
}

public void startPhotoPickerIntent(ValueCallback<Uri> filePathCallback, String acceptType) {
mRNCWebViewModuleImpl.startPhotoPickerIntent(acceptType, filePathCallback);
}
Expand Down
32 changes: 31 additions & 1 deletion example/examples/NativeWebpage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,48 @@ interface Props {}
interface State {}

export default class NativeWebpage extends Component<Props, State> {
state = {};
state = {
modifiedHtml: '',
webViewKey: 1
};

async componentDidMount() {
const html = await this.fetchAndModifyHtml()
this.setState({ modifiedHtml: html, webViewKey: this.state.webViewKey + 1 });
}

fetchAndModifyHtml = async () => {
try {
const response = await fetch('https://infinite.red/');
const html = await response.text();
const modifiedHtml = html.replace('<body>', '<body><p>Request succesfully intercepted!</p>');
return modifiedHtml
} catch (error) {
console.error('Error fetching HTML:', error);

return ""
}
};

render() {
const { modifiedHtml, webViewKey } = this.state;

return (
<View style={{ height: 400 }}>
<WebView
key={webViewKey}
source={{ uri: 'https://infinite.red' }}
style={{ width: '100%', height: '100%' }}
onShouldStartLoadWithRequest={(event) => {
console.log("onShouldStartLoadWithRequest", event);
return true;
}}
onShouldInterceptRequest={(event) => {
console.log("onShouldInterceptRequest", event);
if (event.url === 'https://infinite.red/') {
return modifiedHtml
}
}}
onLoadStart={(event) => {
console.log("onLoadStart", event.nativeEvent);
}}
Expand Down
5 changes: 5 additions & 0 deletions src/NativeRNCWebView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export interface Spec extends TurboModule {
shouldStart: boolean,
lockIdentifier: Double
): void;
shouldInterceptRequestLockIdentifier(
shouldIntercept: boolean,
lockIdentifier: Double,
input?: string,
): void;
}

export default TurboModuleRegistry.getEnforcing<Spec>('RNCWebView');
18 changes: 18 additions & 0 deletions src/RNCWebViewNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ export type ShouldStartLoadRequestEvent = Readonly<{
isTopFrame: boolean;
}>;

export type ShouldInterceptRequestEvent = Readonly<{
url: string;
loading: boolean;
title: string;
canGoBack: boolean;
canGoForward: boolean;
lockIdentifier: Double;
navigationType:
| 'click'
| 'formsubmit'
| 'backforward'
| 'reload'
| 'formresubmit'
| 'other';
mainDocumentURL?: string;
}>;

type ScrollEvent = Readonly<{
contentInset: {
bottom: Double;
Expand Down Expand Up @@ -274,6 +291,7 @@ export interface NativeProps extends ViewProps {
hasOnOpenWindowEvent?: boolean;
onScroll?: DirectEventHandler<ScrollEvent>;
onShouldStartLoadWithRequest: DirectEventHandler<ShouldStartLoadRequestEvent>;
onShouldInterceptRequest: DirectEventHandler<ShouldInterceptRequestEvent>;
showsHorizontalScrollIndicator?: boolean;
showsVerticalScrollIndicator?: boolean;
newSource: Readonly<{
Expand Down

0 comments on commit 4807f2f

Please sign in to comment.