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 5237d92ad..6cac62615 100644 --- a/android/app/src/main/java/org/jitsi/meet/MainActivity.java +++ b/android/app/src/main/java/org/jitsi/meet/MainActivity.java @@ -28,6 +28,7 @@ import org.jitsi.meet.sdk.invite.InviteController; import org.jitsi.meet.sdk.invite.InviteControllerListener; import com.calendarevents.CalendarEventsPackage; +import com.facebook.react.bridge.UiThreadUtil; import java.util.ArrayList; import java.util.HashMap; @@ -65,6 +66,8 @@ public class MainActivity extends JitsiMeetActivity { if (BuildConfig.DEBUG && view != null) { view.setListener(new JitsiMeetViewListener() { private void on(String name, Map data) { + UiThreadUtil.assertOnUiThread(); + // Log with the tag "ReactNative" in order to have the log // visible in react-native log-android as well. Log.d( @@ -112,6 +115,8 @@ public class MainActivity extends JitsiMeetActivity { inviteController.setListener(new InviteControllerListener() { public void beginAddPeople( AddPeopleController addPeopleController) { + UiThreadUtil.assertOnUiThread(); + onInviteControllerBeginAddPeople( inviteController, addPeopleController); @@ -129,6 +134,8 @@ public class MainActivity extends JitsiMeetActivity { private void onAddPeopleControllerInviteSettled( AddPeopleController addPeopleController, List> failedInvitees) { + UiThreadUtil.assertOnUiThread(); + // XXX Explicitly invoke endAddPeople on addPeopleController; otherwise, // it is going to be memory-leaked in the associated InviteController // and no subsequent InviteButton clicks/taps will be delivered. @@ -142,6 +149,8 @@ public class MainActivity extends JitsiMeetActivity { AddPeopleController addPeopleController, List> results, String query) { + UiThreadUtil.assertOnUiThread(); + int size = results.size(); if (size > 0) { @@ -182,6 +191,8 @@ public class MainActivity extends JitsiMeetActivity { private void onInviteControllerBeginAddPeople( InviteController inviteController, AddPeopleController addPeopleController) { + UiThreadUtil.assertOnUiThread(); + // Log with the tag "ReactNative" in order to have the log visible in // react-native log-android as well. Log.d( diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ExternalAPIModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ExternalAPIModule.java index e1ab0fe0f..15c8da856 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/ExternalAPIModule.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ExternalAPIModule.java @@ -21,9 +21,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; - -import org.jitsi.meet.sdk.JitsiMeetView; -import org.jitsi.meet.sdk.JitsiMeetViewListener; +import com.facebook.react.bridge.UiThreadUtil; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -150,43 +148,53 @@ class ExternalAPIModule extends ReactContextBaseJavaModule { * @param scope */ @ReactMethod - public void sendEvent(String name, ReadableMap data, String scope) { - // The JavaScript App needs to provide uniquely identifying information - // to the native ExternalAPI module so that the latter may match the - // former to the native JitsiMeetView which hosts it. - JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope); + public void sendEvent(final String name, + final ReadableMap data, + final String scope) { + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + // The JavaScript App needs to provide uniquely identifying + // information to the native ExternalAPI module so that the + // latter may match the former to the native JitsiMeetView which + // hosts it. + JitsiMeetView view + = JitsiMeetView.findViewByExternalAPIScope(scope); - if (view == null) { - return; - } + if (view == null) { + return; + } - maybeSetViewURL(name, data, view); + maybeSetViewURL(name, data, view); - JitsiMeetViewListener listener = view.getListener(); + JitsiMeetViewListener listener = view.getListener(); - if (listener == null) { - return; - } + if (listener == null) { + return; + } - Method method = JITSI_MEET_VIEW_LISTENER_METHODS.get(name); + Method method = JITSI_MEET_VIEW_LISTENER_METHODS.get(name); - if (method != null) { - try { - method.invoke(listener, toHashMap(data)); - } catch (IllegalAccessException e) { - // FIXME There was a multicatch for IllegalAccessException and - // InvocationTargetException, but Android Studio complained - // with: - // "Multi-catch with these reflection exceptions requires - // API level 19 (current min is 16) because they get compiled to - // the common but new super type ReflectiveOperationException. - // As a workaround either create individual catch statements, or - // catch Exception." - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e); + if (method != null) { + try { + method.invoke(listener, toHashMap(data)); + } catch (IllegalAccessException e) { + // FIXME There was a multicatch for + // IllegalAccessException and InvocationTargetException, + // but Android Studio complained with: "Multi-catch with + // these reflection exceptions requires API level 19 + // (current min is 16) because they get compiled to the + // common but new super type + // ReflectiveOperationException. As a workaround either + // create individual catch statements, or + // catch Exception." + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } } - } + }); } /** diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteModule.java index 0a9957bcc..311f82b72 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteModule.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteModule.java @@ -22,6 +22,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.UiThreadUtil; import org.jitsi.meet.sdk.JitsiMeetView; @@ -42,13 +43,18 @@ public class InviteModule extends ReactContextBaseJavaModule { * {@code JitsiMeetView} whose {@code InviteButton} was clicked/tapped. */ @ReactMethod - public void beginAddPeople(String externalAPIScope) { - InviteController inviteController - = findInviteControllerByExternalAPIScope(externalAPIScope); + public void beginAddPeople(final String externalAPIScope) { + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + InviteController inviteController + = findInviteControllerByExternalAPIScope(externalAPIScope); - if (inviteController != null) { - inviteController.beginAddPeople(getReactApplicationContext()); - } + if (inviteController != null) { + inviteController.beginAddPeople(getReactApplicationContext()); + } + } + }); } private InviteController findInviteControllerByExternalAPIScope( @@ -72,21 +78,26 @@ public class InviteModule extends ReactContextBaseJavaModule { */ @ReactMethod public void inviteSettled( - String externalAPIScope, - String addPeopleControllerScope, - ReadableArray failedInvitees) { - InviteController inviteController - = findInviteControllerByExternalAPIScope(externalAPIScope); + final String externalAPIScope, + final String addPeopleControllerScope, + final ReadableArray failedInvitees) { + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + InviteController inviteController + = findInviteControllerByExternalAPIScope(externalAPIScope); - if (inviteController == null) { - Log.w( - "InviteModule", - "Invite settled, but failed to find active controller to notify"); - } else { - inviteController.inviteSettled( - addPeopleControllerScope, - failedInvitees); - } + if (inviteController == null) { + Log.w( + "InviteModule", + "Invite settled, but failed to find active controller to notify"); + } else { + inviteController.inviteSettled( + addPeopleControllerScope, + failedInvitees); + } + } + }); } /** @@ -98,22 +109,27 @@ public class InviteModule extends ReactContextBaseJavaModule { */ @ReactMethod public void receivedResults( - String externalAPIScope, - String addPeopleControllerScope, - String query, - ReadableArray results) { - InviteController inviteController - = findInviteControllerByExternalAPIScope(externalAPIScope); + final String externalAPIScope, + final String addPeopleControllerScope, + final String query, + final ReadableArray results) { + UiThreadUtil.runOnUiThread(new Runnable() { + @Override + public void run() { + InviteController inviteController + = findInviteControllerByExternalAPIScope(externalAPIScope); - if (inviteController == null) { - Log.w( - "InviteModule", - "Received results, but failed to find active controller to send results back"); - } else { - inviteController.receivedResultsForQuery( - addPeopleControllerScope, - query, - results); - } + if (inviteController == null) { + Log.w( + "InviteModule", + "Received results, but failed to find active controller to send results back"); + } else { + inviteController.receivedResultsForQuery( + addPeopleControllerScope, + query, + results); + } + } + }); } } diff --git a/ios/app/src/ViewController.m b/ios/app/src/ViewController.m index 7dadfcd3b..cb8a38227 100644 --- a/ios/app/src/ViewController.m +++ b/ios/app/src/ViewController.m @@ -14,6 +14,8 @@ * limitations under the License. */ +#import + #import "ViewController.h" /** @@ -64,6 +66,9 @@ void _onJitsiMeetViewDelegateEvent(NSString *name, NSDictionary *data) { NSLog( @"[%s:%d] JitsiMeetViewDelegate %@ %@", __FILE__, __LINE__, name, data); + + assert([NSThread isMainThread] + && "Delegate method called in a non-main thread"); } - (void)conferenceFailed:(NSDictionary *)data { @@ -97,6 +102,9 @@ void _onJitsiMeetViewDelegateEvent(NSString *name, NSDictionary *data) { @"[%s:%d] JMInviteControllerDelegate %s", __FILE__, __LINE__, __FUNCTION__); + assert([NSThread isMainThread] + && "Delegate method called in a non-main thread"); + NSString *query = ADD_PEOPLE_CONTROLLER_QUERY; JitsiMeetView *view = (JitsiMeetView *) self.view; JMInviteController *inviteController = view.inviteController; @@ -119,6 +127,9 @@ void _onJitsiMeetViewDelegateEvent(NSString *name, NSDictionary *data) { - (void)addPeopleController:(JMAddPeopleController * _Nonnull)controller didReceiveResults:(NSArray * _Nonnull)results forQuery:(NSString * _Nonnull)query { + assert([NSThread isMainThread] + && "Delegate method called in a non-main thread"); + NSUInteger count = results.count; if (count) { @@ -151,6 +162,9 @@ void _onJitsiMeetViewDelegateEvent(NSString *name, NSDictionary *data) { - (void) inviteSettled:(NSArray * _Nonnull)failedInvitees fromSearchController:(JMAddPeopleController * _Nonnull)addPeopleController { + assert([NSThread isMainThread] + && "Delegate method called in a non-main thread"); + // XXX Explicitly invoke endAddPeople on addPeopleController; otherwise, it // is going to be memory-leaked in the associated JMInviteController and no // subsequent InviteButton clicks/taps will be delivered. Technically, diff --git a/ios/sdk/src/ExternalAPI.m b/ios/sdk/src/ExternalAPI.m index cce03faca..5c9cc4c39 100644 --- a/ios/sdk/src/ExternalAPI.m +++ b/ios/sdk/src/ExternalAPI.m @@ -25,6 +25,13 @@ RCT_EXPORT_MODULE(); +/** + * Make sure all methods in this module are called in the main (i.e. UI) thread. + */ +- (dispatch_queue_t)methodQueue { + return dispatch_get_main_queue(); +} + /** * Dispatches an event that occurred on JavaScript to the view's delegate. * diff --git a/ios/sdk/src/invite/Invite.m b/ios/sdk/src/invite/Invite.m index ea473f278..752583b44 100644 --- a/ios/sdk/src/invite/Invite.m +++ b/ios/sdk/src/invite/Invite.m @@ -40,6 +40,13 @@ RCT_EXPORT_MODULE(); ]; } +/** + * Make sure all methods in this module are called in the main (i.e. UI) thread. + */ +- (dispatch_queue_t)methodQueue { + return dispatch_get_main_queue(); +} + /** * Initiates the process to add people. This involves calling a delegate method * in the JMInviteControllerDelegate so the native host application can start