diff --git a/android/app/src/main/java/org/jitsi/meet/MainActivity.java b/android/app/src/main/java/org/jitsi/meet/MainActivity.java index 181602b1d..f9876443f 100644 --- a/android/app/src/main/java/org/jitsi/meet/MainActivity.java +++ b/android/app/src/main/java/org/jitsi/meet/MainActivity.java @@ -1,11 +1,8 @@ package org.jitsi.meet; -import android.os.Bundle; -import com.crashlytics.android.Crashlytics; import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; import com.facebook.react.ReactRootView; -import io.fabric.sdk.android.Fabric; public class MainActivity extends ReactActivity { /** @@ -46,11 +43,4 @@ public class MainActivity extends ReactActivity { protected String getMainComponentName() { return "App"; } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Fabric.with(this, new Crashlytics()); - } } diff --git a/android/app/src/main/java/org/jitsi/meet/MainApplication.java b/android/app/src/main/java/org/jitsi/meet/MainApplication.java index 850a195fa..5f36b87f0 100644 --- a/android/app/src/main/java/org/jitsi/meet/MainApplication.java +++ b/android/app/src/main/java/org/jitsi/meet/MainApplication.java @@ -1,39 +1,60 @@ package org.jitsi.meet; import android.app.Application; -import android.util.Log; +import com.crashlytics.android.Crashlytics; import com.facebook.react.ReactApplication; -import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; -import com.facebook.react.shell.MainReactPackage; -import com.oblador.vectoricons.VectorIconsPackage; -import com.oney.WebRTCModule.WebRTCModulePackage; + +import io.fabric.sdk.android.Fabric; import java.util.Arrays; import java.util.List; public class MainApplication extends Application implements ReactApplication { + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + /** + * {@inheritDoc} + */ + @Override + protected boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } - private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + /** + * {@inheritDoc} + */ + @Override + protected List getPackages() { + return Arrays.asList( + new com.facebook.react.shell.MainReactPackage(), + new com.oblador.vectoricons.VectorIconsPackage(), + new com.oney.WebRTCModule.WebRTCModulePackage() + ); + } + }; + + /** + * {@inheritDoc} + */ @Override - protected boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; } + /** + * {@inheritDoc} + */ @Override - protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new VectorIconsPackage(), - new WebRTCModulePackage() - ); - } - }; + public void onCreate() { + super.onCreate(); - @Override - public ReactNativeHost getReactNativeHost() { - return mReactNativeHost; - } + if (!getReactNativeHost() + .getReactInstanceManager() + .getDevSupportManager() + .getDevSupportEnabled()) { + Fabric.with(this, new Crashlytics()); + } + } } diff --git a/ios/app/AppDelegate.m b/ios/app/AppDelegate.m index 1632d759b..da7ed8ac6 100644 --- a/ios/app/AppDelegate.m +++ b/ios/app/AppDelegate.m @@ -10,10 +10,35 @@ #import "AppDelegate.h" #import #import +#import "RCTAssert.h" #import "RCTBundleURLProvider.h" #import "RCTLinkingManager.h" #import "RCTRootView.h" +/** + * A RCTFatalHandler implementation which swallows JavaScript errors. + * In the Release configuration, React Native will (intentionally) raise an + * unhandled NSException for an unhandled JavaScript error. This will + * effectively kill the application. _RCTFatal is suitable to be in + * accord with the Web i.e. not kill the application. + */ +RCTFatalHandler _RCTFatal = ^(NSError *error) { + id jsStackTrace = error.userInfo[RCTJSStackTraceKey]; + @try { + NSString *name + = [NSString stringWithFormat:@"%@: %@", + RCTFatalExceptionName, + error.localizedDescription]; + NSString *message + = RCTFormatError(error.localizedDescription, jsStackTrace, 75); + [NSException raise:name format:@"%@", message]; + } @catch (NSException *e) { + if (!jsStackTrace) { + @throw; + } + } +}; + @implementation AppDelegate // https://facebook.github.io/react-native/docs/linking.html @@ -29,8 +54,18 @@ continueUserActivity:(NSUserActivity *)userActivity - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { +#if !DEBUG [Fabric with:@[[Crashlytics class]]]; + // In the Release configuration, React Native will (intentionally) raise an + // unhandled NSException for an unhandled JavaScript error. This will + // effectively kill the application. In accord with the Web, do not kill the + // application. + if (!RCTGetFatalHandler()) { + RCTSetFatalHandler(_RCTFatal); + } +#endif + NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; diff --git a/ios/app/POSIX.m b/ios/app/POSIX.m index 9e73925c6..8f6af0ff7 100644 --- a/ios/app/POSIX.m +++ b/ios/app/POSIX.m @@ -53,8 +53,8 @@ RCT_EXPORT_METHOD(getaddrinfo:(NSString *)hostname if (0 != err) { NSError *error = [NSError errorWithDomain:NSPOSIXErrorDomain - code:err - userInfo:nil]; + code:err + userInfo:nil]; reject(rejectCode, error.localizedDescription, error); } diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js index acc22895d..a4a3d8092 100644 --- a/react/features/app/components/App.native.js +++ b/react/features/app/components/App.native.js @@ -1,5 +1,7 @@ +/* global __DEV__ */ + import React from 'react'; -import { Linking, Navigator } from 'react-native'; +import { Linking, Navigator, Platform } from 'react-native'; import { Provider } from 'react-redux'; import { _getRouteToRender } from '../functions'; @@ -30,6 +32,12 @@ export class App extends AbstractApp { // Bind event handlers so they are only bound once for every instance. this._navigatorRenderScene = this._navigatorRenderScene.bind(this); this._onLinkingURL = this._onLinkingURL.bind(this); + + // In the Release configuration, React Native will (intentionally) throw + // an unhandled JavascriptException for an unhandled JavaScript error. + // This will effectively kill the application. In accord with the Web, + // do not kill the application. + this._maybeDisableExceptionsManager(); } /** @@ -101,6 +109,44 @@ export class App extends AbstractApp { navigator && navigator.replace({ ...route }); } + /** + * Attempts to disable the use of React Native + * {@link ExceptionsManager#handleException} on platforms and in + * configurations on/in which the use of the method in questions has been + * determined to be undesirable. For example, React Native will + * (intentionally) throw an unhandled JavascriptException for an + * unhandled JavaScript error in the Release configuration. This will + * effectively kill the application. In accord with the Web, do not kill the + * application. + * + * @private + * @returns {void} + */ + _maybeDisableExceptionsManager() { + if (__DEV__) { + // As mentioned above, only the Release configuration was observed + // to suffer. + return; + } + if (Platform.OS !== 'android') { + // A solution based on RTCSetFatalHandler was implemented on iOS and + // it is preferred because it is at a later step of the + // error/exception handling and it is specific to fatal + // errors/exceptions which were observed to kill the application. + // The solution implemented bellow was tested on Android only so it + // is considered safest to use it there only. + return; + } + + const oldHandler = global.ErrorUtils.getGlobalHandler(); + const newHandler = _handleException; + + if (!oldHandler || oldHandler !== newHandler) { + newHandler.next = oldHandler; + global.ErrorUtils.setGlobalHandler(newHandler); + } + } + /** * Renders the scene identified by a specific route in the Navigator of this * instance. @@ -133,3 +179,29 @@ export class App extends AbstractApp { this._openURL(event.url); } } + +/** + * Handles a (possibly unhandled) JavaScript error by preventing React Native + * from converting a fatal error into an unhandled native exception which will + * kill the application. + * + * @param {Error} error - The (possibly unhandled) JavaScript error to handle. + * @param {boolean} fatal - True if the specified error is fatal; otherwise, + * false. + * @private + * @returns {void} + */ +function _handleException(error, fatal) { + if (fatal) { + // In the Release configuration, React Native will (intentionally) throw + // an unhandled JavascriptException for an unhandled JavaScript error. + // This will effectively kill the application. In accord with the Web, + // do not kill the application. + console.error(error); + } else { + // Forward to the next globalHandler of ErrorUtils. + const next = _handleException.next; + + typeof next === 'function' && next(error, fatal); + } +}