Browse Source

feat(android): WebView crash handling (#1480)

Co-authored-by: Cristiano Coelho <cristianocca@hotmail.com>
cristianoccazinsp 4 years ago
parent
commit
8a8b7ceb98
No account linked to committer's email address

+ 43
- 4
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java View File

17
 import androidx.annotation.RequiresApi;
17
 import androidx.annotation.RequiresApi;
18
 import androidx.core.content.ContextCompat;
18
 import androidx.core.content.ContextCompat;
19
 import android.text.TextUtils;
19
 import android.text.TextUtils;
20
+import android.util.Log;
20
 import android.view.Gravity;
21
 import android.view.Gravity;
21
 import android.view.View;
22
 import android.view.View;
22
 import android.view.ViewGroup;
23
 import android.view.ViewGroup;
27
 import android.webkit.DownloadListener;
28
 import android.webkit.DownloadListener;
28
 import android.webkit.GeolocationPermissions;
29
 import android.webkit.GeolocationPermissions;
29
 import android.webkit.JavascriptInterface;
30
 import android.webkit.JavascriptInterface;
31
+import android.webkit.RenderProcessGoneDetail;
30
 import android.webkit.SslErrorHandler;
32
 import android.webkit.SslErrorHandler;
31
 import android.webkit.PermissionRequest;
33
 import android.webkit.PermissionRequest;
32
 import android.webkit.URLUtil;
34
 import android.webkit.URLUtil;
69
 import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
71
 import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
70
 import com.reactnativecommunity.webview.events.TopMessageEvent;
72
 import com.reactnativecommunity.webview.events.TopMessageEvent;
71
 import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
73
 import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
74
+import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent;
72
 
75
 
73
 import org.json.JSONException;
76
 import org.json.JSONException;
74
 import org.json.JSONObject;
77
 import org.json.JSONObject;
575
     export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
578
     export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
576
     export.put(ScrollEventType.getJSEventName(ScrollEventType.SCROLL), MapBuilder.of("registrationName", "onScroll"));
579
     export.put(ScrollEventType.getJSEventName(ScrollEventType.SCROLL), MapBuilder.of("registrationName", "onScroll"));
577
     export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError"));
580
     export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError"));
581
+    export.put(TopRenderProcessGoneEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRenderProcessGone"));
578
     return export;
582
     return export;
579
   }
583
   }
580
 
584
 
771
       mLastLoadFailed = false;
775
       mLastLoadFailed = false;
772
 
776
 
773
       RNCWebView reactWebView = (RNCWebView) webView;
777
       RNCWebView reactWebView = (RNCWebView) webView;
774
-      reactWebView.callInjectedJavaScriptBeforeContentLoaded();       
778
+      reactWebView.callInjectedJavaScriptBeforeContentLoaded();
775
 
779
 
776
       dispatchEvent(
780
       dispatchEvent(
777
         webView,
781
         webView,
828
           case SslError.SSL_UNTRUSTED:
832
           case SslError.SSL_UNTRUSTED:
829
             description = "The certificate authority is not trusted";
833
             description = "The certificate authority is not trusted";
830
             break;
834
             break;
831
-          default: 
835
+          default:
832
             description = "Unknown SSL Error";
836
             description = "Unknown SSL Error";
833
             break;
837
             break;
834
         }
838
         }
835
-        
839
+
836
         description = descriptionPrefix + description;
840
         description = descriptionPrefix + description;
837
 
841
 
838
         this.onReceivedError(
842
         this.onReceivedError(
842
           failingUrl
846
           failingUrl
843
         );
847
         );
844
     }
848
     }
845
-    
849
+
846
     @Override
850
     @Override
847
     public void onReceivedError(
851
     public void onReceivedError(
848
       WebView webView,
852
       WebView webView,
899
       }
903
       }
900
     }
904
     }
901
 
905
 
906
+    @TargetApi(Build.VERSION_CODES.O)
907
+    @Override
908
+    public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) {
909
+        // WebViewClient.onRenderProcessGone was added in O.
910
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
911
+            return false;
912
+        }
913
+        super.onRenderProcessGone(webView, detail);
914
+
915
+        if(detail.didCrash()){
916
+          Log.e("RNCWebViewManager", "The WebView rendering process crashed.");
917
+        }
918
+        else{
919
+          Log.w("RNCWebViewManager", "The WebView rendering process was killed by the system.");
920
+        }
921
+
922
+        // if webView is null, we cannot return any event
923
+        // since the view is already dead/disposed
924
+        // still prevent the app crash by returning true.
925
+        if(webView == null){
926
+          return true;
927
+        }
928
+
929
+        WritableMap event = createWebViewEvent(webView, webView.getUrl());
930
+        event.putBoolean("didCrash", detail.didCrash());
931
+
932
+        dispatchEvent(
933
+          webView,
934
+          new TopRenderProcessGoneEvent(webView.getId(), event)
935
+        );
936
+
937
+        // returning false would crash the app.
938
+        return true;
939
+    }
940
+
902
     protected void emitFinishEvent(WebView webView, String url) {
941
     protected void emitFinishEvent(WebView webView, String url) {
903
       dispatchEvent(
942
       dispatchEvent(
904
         webView,
943
         webView,

+ 26
- 0
android/src/main/java/com/reactnativecommunity/webview/events/TopRenderProcessGoneEvent.kt View File

1
+package com.reactnativecommunity.webview.events
2
+
3
+import com.facebook.react.bridge.WritableMap
4
+import com.facebook.react.uimanager.events.Event
5
+import com.facebook.react.uimanager.events.RCTEventEmitter
6
+
7
+/**
8
+ * Event emitted when the WebView's process has crashed or
9
+   was killed by the OS.
10
+ */
11
+class TopRenderProcessGoneEvent(viewId: Int, private val mEventData: WritableMap) :
12
+  Event<TopRenderProcessGoneEvent>(viewId) {
13
+  companion object {
14
+    const val EVENT_NAME = "topRenderProcessGone"
15
+  }
16
+
17
+  override fun getEventName(): String = EVENT_NAME
18
+
19
+  override fun canCoalesce(): Boolean = false
20
+
21
+  override fun getCoalescingKey(): Short = 0
22
+
23
+  override fun dispatch(rctEventEmitter: RCTEventEmitter) =
24
+    rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
25
+
26
+}

+ 34
- 0
docs/Reference.md View File

13
 - [`mediaPlaybackRequiresUserAction`](Reference.md#mediaplaybackrequiresuseraction)
13
 - [`mediaPlaybackRequiresUserAction`](Reference.md#mediaplaybackrequiresuseraction)
14
 - [`nativeConfig`](Reference.md#nativeconfig)
14
 - [`nativeConfig`](Reference.md#nativeconfig)
15
 - [`onError`](Reference.md#onerror)
15
 - [`onError`](Reference.md#onerror)
16
+- [`onRenderProcessGone`](Reference.md#onRenderProcessGone)
16
 - [`onLoad`](Reference.md#onload)
17
 - [`onLoad`](Reference.md#onload)
17
 - [`onLoadEnd`](Reference.md#onloadend)
18
 - [`onLoadEnd`](Reference.md#onloadend)
18
 - [`onLoadStart`](Reference.md#onloadstart)
19
 - [`onLoadStart`](Reference.md#onloadstart)
458
 
459
 
459
 ---
460
 ---
460
 
461
 
462
+### `onRenderProcessGone`[⬆](#props-index)<!-- Link generated with jump2header -->
463
+
464
+Function that is invoked when the `WebView` process crashes or is killed by the OS on Android.
465
+
466
+> **_Note_**
467
+> Android API minimum level 26. Android Only
468
+
469
+| Type     | Required |
470
+| -------- | -------- |
471
+| function | No       |
472
+
473
+Example:
474
+
475
+```jsx
476
+<WebView
477
+  source={{ uri: 'https://reactnative.dev' }}
478
+  onRenderProcessGone={syntheticEvent => {
479
+    const { nativeEvent } = syntheticEvent;
480
+    console.warn(
481
+      'WebView Crashed: ',
482
+      nativeEvent.didCrash,
483
+    );
484
+  }}
485
+/>
486
+```
487
+
488
+Function passed to `onRenderProcessGone` is called with a SyntheticEvent wrapping a nativeEvent with these properties:
489
+
490
+```
491
+didCrash
492
+```
493
+---
494
+
461
 ### `onMessage`[⬆](#props-index)<!-- Link generated with jump2header -->
495
 ### `onMessage`[⬆](#props-index)<!-- Link generated with jump2header -->
462
 
496
 
463
 Function that is invoked when the webview calls `window.ReactNativeWebView.postMessage`. Setting this property will inject this global into your webview.
497
 Function that is invoked when the webview calls `window.ReactNativeWebView.postMessage`. Setting this property will inject this global into your webview.

+ 9
- 0
src/WebView.android.tsx View File

21
   defaultRenderLoading,
21
   defaultRenderLoading,
22
 } from './WebViewShared';
22
 } from './WebViewShared';
23
 import {
23
 import {
24
+  WebViewRenderProcessGoneEvent,
24
   WebViewErrorEvent,
25
   WebViewErrorEvent,
25
   WebViewHttpErrorEvent,
26
   WebViewHttpErrorEvent,
26
   WebViewMessageEvent,
27
   WebViewMessageEvent,
228
     }
229
     }
229
   }
230
   }
230
 
231
 
232
+  onRenderProcessGone = (event: WebViewRenderProcessGoneEvent) => {
233
+    const { onRenderProcessGone } = this.props;
234
+    if (onRenderProcessGone) {
235
+      onRenderProcessGone(event);
236
+    }
237
+  }
238
+
231
   onLoadingFinish = (event: WebViewNavigationEvent) => {
239
   onLoadingFinish = (event: WebViewNavigationEvent) => {
232
     const { onLoad, onLoadEnd } = this.props;
240
     const { onLoad, onLoadEnd } = this.props;
233
     const { nativeEvent: { url } } = event;
241
     const { nativeEvent: { url } } = event;
347
         onLoadingProgress={this.onLoadingProgress}
355
         onLoadingProgress={this.onLoadingProgress}
348
         onLoadingStart={this.onLoadingStart}
356
         onLoadingStart={this.onLoadingStart}
349
         onHttpError={this.onHttpError}
357
         onHttpError={this.onHttpError}
358
+        onRenderProcessGone={this.onRenderProcessGone}
350
         onMessage={this.onMessage}
359
         onMessage={this.onMessage}
351
         onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
360
         onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
352
         ref={this.webViewRef}
361
         ref={this.webViewRef}

+ 14
- 1
src/WebViewTypes.ts View File

137
   statusCode: number;
137
   statusCode: number;
138
 }
138
 }
139
 
139
 
140
+export interface WebViewRenderProcessGoneDetail {
141
+  didCrash: boolean;
142
+}
143
+
140
 export type WebViewEvent = NativeSyntheticEvent<WebViewNativeEvent>;
144
 export type WebViewEvent = NativeSyntheticEvent<WebViewNativeEvent>;
141
 
145
 
142
 export type WebViewProgressEvent = NativeSyntheticEvent<
146
 export type WebViewProgressEvent = NativeSyntheticEvent<
155
 
159
 
156
 export type WebViewHttpErrorEvent = NativeSyntheticEvent<WebViewHttpError>;
160
 export type WebViewHttpErrorEvent = NativeSyntheticEvent<WebViewHttpError>;
157
 
161
 
162
+export type WebViewRenderProcessGoneEvent = NativeSyntheticEvent<WebViewRenderProcessGoneDetail>;
163
+
158
 export type DataDetectorTypes =
164
 export type DataDetectorTypes =
159
   | 'phoneNumber'
165
   | 'phoneNumber'
160
   | 'link'
166
   | 'link'
277
   javaScriptEnabled?: boolean;
283
   javaScriptEnabled?: boolean;
278
   mixedContentMode?: 'never' | 'always' | 'compatibility';
284
   mixedContentMode?: 'never' | 'always' | 'compatibility';
279
   onContentSizeChange?: (event: WebViewEvent) => void;
285
   onContentSizeChange?: (event: WebViewEvent) => void;
286
+  onRenderProcessGone?: (event: WebViewRenderProcessGoneEvent) => void;
280
   overScrollMode?: OverScrollModeType;
287
   overScrollMode?: OverScrollModeType;
281
   saveFormDataDisabled?: boolean;
288
   saveFormDataDisabled?: boolean;
282
   textZoom?: number;
289
   textZoom?: number;
683
   onNavigationStateChange?: (event: WebViewNavigation) => void;
690
   onNavigationStateChange?: (event: WebViewNavigation) => void;
684
   onContentSizeChange?: (event: WebViewEvent) => void;
691
   onContentSizeChange?: (event: WebViewEvent) => void;
685
 
692
 
693
+  /**
694
+   * Function that is invoked when the `WebView` process crashes or is killed by the OS.
695
+   * Works only on Android (minimum API level 26).
696
+   */
697
+  onRenderProcessGone?: (event: WebViewRenderProcessGoneEvent) => void;
698
+
686
   /**
699
   /**
687
    * https://developer.android.com/reference/android/webkit/WebSettings.html#setCacheMode(int)
700
    * https://developer.android.com/reference/android/webkit/WebSettings.html#setCacheMode(int)
688
    * Set the cacheMode. Possible values are:
701
    * Set the cacheMode. Possible values are:
721
    */
734
    */
722
   geolocationEnabled?: boolean;
735
   geolocationEnabled?: boolean;
723
 
736
 
724
-  
737
+
725
   /**
738
   /**
726
    * Boolean that sets whether JavaScript running in the context of a file
739
    * Boolean that sets whether JavaScript running in the context of a file
727
    * scheme URL should be allowed to access content from other file scheme URLs.
740
    * scheme URL should be allowed to access content from other file scheme URLs.