Browse Source

[Android] Split base functionality out of JitsiMeetView

As the need for adding more views connected with our React code arises, having
everything in JitsiMeetView is not going to scale.

In order to pave the way for multiple apps / views feeding off the React side,
the following changes have been made:

- All base functionality related to creating a ReactRootView and layout are now
  in BaseReactView
- All Activity lifecycle methods that need to be called by any activity holding
  a BaseReactView are now conveniently placed in ReactActivityLifecycleAdapter
- ExternalAPIModule has been refactored to cater for multiple views: events are
  delivered to views, and its their resposibility to deal with them
- Following on the previous point, ListenerUtils is a utility class for helping
  with the translation from events into listener methods
master
Saúl Ibarra Corretgé 6 years ago
parent
commit
9972e88b67

+ 171
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/BaseReactView.java View File

1
+package org.jitsi.meet.sdk;
2
+
3
+import android.app.Activity;
4
+import android.content.Context;
5
+import android.os.Bundle;
6
+import android.support.annotation.NonNull;
7
+import android.support.annotation.Nullable;
8
+import android.util.Log;
9
+import android.widget.FrameLayout;
10
+
11
+import com.facebook.react.ReactRootView;
12
+import com.facebook.react.bridge.ReadableMap;
13
+import com.rnimmersive.RNImmersiveModule;
14
+
15
+import java.util.Collections;
16
+import java.util.Set;
17
+import java.util.UUID;
18
+import java.util.WeakHashMap;
19
+
20
+/**
21
+ * Base class for all views which are backed by a React Native view.
22
+ */
23
+public abstract class BaseReactView extends FrameLayout {
24
+    /**
25
+     * Background color used by {@code BaseReactView} and the React Native root
26
+     * view.
27
+     */
28
+    protected static int BACKGROUND_COLOR = 0xFF111111;
29
+
30
+    /**
31
+     * The unique identifier of this {@code BaseReactView} within the process
32
+     * for the purposes of {@link ExternalAPIModule}. The name scope was
33
+     * inspired by postis which we use on Web for the similar purposes of the
34
+     * iframe-based external API.
35
+     */
36
+    protected final String externalAPIScope;
37
+
38
+    /**
39
+     * React Native root view.
40
+     */
41
+    private ReactRootView reactRootView;
42
+
43
+    /**
44
+     * Collection with all created views. This is used for finding the right
45
+     * view when delivering events coming from the {@link ExternalAPIModule};
46
+     */
47
+    static final Set<BaseReactView> views
48
+        = Collections.newSetFromMap(new WeakHashMap<BaseReactView, Boolean>());
49
+
50
+    /**
51
+     * Find a view which matches the given external API scope.
52
+     *
53
+     * @param externalAPIScope - Scope for the view we want to find.
54
+     * @return The found {@code BaseReactView}, or {@code null}.
55
+     */
56
+    public static BaseReactView findViewByExternalAPIScope(
57
+            String externalAPIScope) {
58
+        synchronized (views) {
59
+            for (BaseReactView view : views) {
60
+                if (view.externalAPIScope.equals(externalAPIScope)) {
61
+                    return view;
62
+                }
63
+            }
64
+        }
65
+
66
+        return null;
67
+    }
68
+
69
+    public BaseReactView(@NonNull Context context) {
70
+        super(context);
71
+
72
+        setBackgroundColor(BACKGROUND_COLOR);
73
+
74
+        ReactInstanceManagerHolder.initReactInstanceManager(
75
+            ((Activity) context).getApplication());
76
+
77
+        // Hook this BaseReactView into ExternalAPI.
78
+        externalAPIScope = UUID.randomUUID().toString();
79
+        synchronized (views) {
80
+            views.add(this);
81
+        }
82
+    }
83
+
84
+    /**
85
+     * Creates the {@code ReactRootView} for the given app name with the given
86
+     * props. Once created it's set as the view of this {@code FrameLayout}.
87
+     *
88
+     * @param appName - Name of the "app" (in React Native terms) which we want
89
+     *                to load.
90
+     * @param props - Props (in React terms) to be passed to the app.
91
+     */
92
+    public void createReactRootView(String appName, @Nullable Bundle props) {
93
+        if (props == null) {
94
+            props = new Bundle();
95
+        }
96
+
97
+        // Set externalAPIScope
98
+        props.putString("externalAPIScope", externalAPIScope);
99
+
100
+        if (reactRootView == null) {
101
+            reactRootView = new ReactRootView(getContext());
102
+            reactRootView.startReactApplication(
103
+                ReactInstanceManagerHolder.getReactInstanceManager(),
104
+                appName,
105
+                props);
106
+            reactRootView.setBackgroundColor(BACKGROUND_COLOR);
107
+            addView(reactRootView);
108
+        } else {
109
+            reactRootView.setAppProperties(props);
110
+        }
111
+    }
112
+
113
+    /**
114
+     * Releases the React resources (specifically the {@link ReactRootView})
115
+     * associated with this view.
116
+     *
117
+     * This method MUST be called when the Activity holding this view is
118
+     * destroyed, typically in the {@code onDestroy} method.
119
+     */
120
+    public void dispose() {
121
+        if (reactRootView != null) {
122
+            removeView(reactRootView);
123
+            reactRootView.unmountReactApplication();
124
+            reactRootView = null;
125
+        }
126
+    }
127
+
128
+    /**
129
+     * Abstract method called by {@link ExternalAPIModule} when an event is
130
+     * received for this view.
131
+     *
132
+     * @param name - Name of the event.
133
+     * @param data - Event data.
134
+     */
135
+    public abstract void onExternalAPIEvent(String name, ReadableMap data);
136
+
137
+    /**
138
+     * Called when the window containing this view gains or loses focus.
139
+     *
140
+     * @param hasFocus If the window of this view now has focus, {@code true};
141
+     * otherwise, {@code false}.
142
+     */
143
+    @Override
144
+    public void onWindowFocusChanged(boolean hasFocus) {
145
+        super.onWindowFocusChanged(hasFocus);
146
+
147
+        // https://github.com/mockingbot/react-native-immersive#restore-immersive-state
148
+
149
+        // FIXME The singleton pattern employed by RNImmersiveModule is not
150
+        // advisable because a react-native mobule is consumable only after its
151
+        // BaseJavaModule#initialize() has completed and here we have no
152
+        // knowledge of whether the precondition is really met.
153
+        RNImmersiveModule immersive = RNImmersiveModule.getInstance();
154
+
155
+        if (hasFocus && immersive != null) {
156
+            try {
157
+                immersive.emitImmersiveStateChangeEvent();
158
+            } catch (RuntimeException re) {
159
+                // FIXME I don't know how to check myself whether
160
+                // BaseJavaModule#initialize() has been invoked and thus
161
+                // RNImmersiveModule is consumable. A safe workaround is to
162
+                // swallow the failure because the whole full-screen/immersive
163
+                // functionality is brittle anyway, akin to the icing on the
164
+                // cake, and has been working without onWindowFocusChanged for a
165
+                // very long time.
166
+                Log.e("RNImmersiveModule",
167
+                    "emitImmersiveStateChangeEvent() failed!", re);
168
+            }
169
+        }
170
+    }
171
+}

+ 11
- 183
android/sdk/src/main/java/org/jitsi/meet/sdk/ExternalAPIModule.java View File

16
 
16
 
17
 package org.jitsi.meet.sdk;
17
 package org.jitsi.meet.sdk;
18
 
18
 
19
+import android.util.Log;
20
+
19
 import com.facebook.react.bridge.ReactApplicationContext;
21
 import com.facebook.react.bridge.ReactApplicationContext;
20
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
22
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
21
 import com.facebook.react.bridge.ReactMethod;
23
 import com.facebook.react.bridge.ReactMethod;
22
 import com.facebook.react.bridge.ReadableMap;
24
 import com.facebook.react.bridge.ReadableMap;
23
-import com.facebook.react.bridge.ReadableMapKeySetIterator;
24
-import com.facebook.react.bridge.UiThreadUtil;
25
-
26
-import java.lang.reflect.InvocationTargetException;
27
-import java.lang.reflect.Method;
28
-import java.lang.reflect.Modifier;
29
-import java.util.HashMap;
30
-import java.util.Locale;
31
-import java.util.Map;
32
-import java.util.regex.Pattern;
33
 
25
 
34
 /**
26
 /**
35
- * Module implementing a simple API to enable a proximity sensor-controlled
36
- * wake lock. When the lock is held, if the proximity sensor detects a nearby
37
- * object it will dim the screen and disable touch controls. The functionality
38
- * is used with the conference audio-only mode.
27
+ * Module implementing an API for sending events from JavaScript to native code.
39
  */
28
  */
40
 class ExternalAPIModule extends ReactContextBaseJavaModule {
29
 class ExternalAPIModule extends ReactContextBaseJavaModule {
41
-    /**
42
-     * The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
43
-     * redux action types.
44
-     */
45
-    private static final Map<String, Method> JITSI_MEET_VIEW_LISTENER_METHODS
46
-        = new HashMap<>();
47
-
48
-    static {
49
-        // Figure out the mapping between the JitsiMeetViewListener methods
50
-        // and the events i.e. redux action types.
51
-        Pattern onPattern = Pattern.compile("^on[A-Z]+");
52
-        Pattern camelcasePattern = Pattern.compile("([a-z0-9]+)([A-Z0-9]+)");
53
-
54
-        for (Method method : JitsiMeetViewListener.class.getDeclaredMethods()) {
55
-            // * The method must be public (because it is declared by an
56
-            //   interface).
57
-            // * The method must be/return void.
58
-            if (!Modifier.isPublic(method.getModifiers())
59
-                    || !Void.TYPE.equals(method.getReturnType())) {
60
-                continue;
61
-            }
62
-
63
-            // * The method name must start with "on" followed by a
64
-            //   capital/uppercase letter (in agreement with the camelcase
65
-            //   coding style customary to Java in general and the projects of
66
-            //   the Jitsi community in particular).
67
-            String name = method.getName();
68
-
69
-            if (!onPattern.matcher(name).find()) {
70
-                continue;
71
-            }
72
-
73
-            // * The method must accept/have exactly 1 parameter of a type
74
-            //   assignable from HashMap.
75
-            Class<?>[] parameterTypes = method.getParameterTypes();
76
-
77
-            if (parameterTypes.length != 1
78
-                    || !parameterTypes[0].isAssignableFrom(HashMap.class)) {
79
-                continue;
80
-            }
81
-
82
-            // Convert the method name to an event name.
83
-            name
84
-                = camelcasePattern.matcher(name.substring(2))
85
-                    .replaceAll("$1_$2")
86
-                    .toUpperCase(Locale.ROOT);
87
-            JITSI_MEET_VIEW_LISTENER_METHODS.put(name, method);
88
-        }
89
-    }
30
+    private static final String TAG = ExternalAPIModule.class.getSimpleName();
90
 
31
 
91
     /**
32
     /**
92
      * Initializes a new module instance. There shall be a single instance of
33
      * Initializes a new module instance. There shall be a single instance of
109
         return "ExternalAPI";
50
         return "ExternalAPI";
110
     }
51
     }
111
 
52
 
112
-    /**
113
-     * The internal processing for the URL of the current conference set on the
114
-     * associated {@link JitsiMeetView}.
115
-     *
116
-     * @param eventName the name of the external API event to be processed
117
-     * @param eventData the details/specifics of the event to process determined
118
-     * by/associated with the specified {@code eventName}.
119
-     * @param view the {@link JitsiMeetView} instance.
120
-     */
121
-    private void maybeSetViewURL(
122
-            String eventName,
123
-            ReadableMap eventData,
124
-            JitsiMeetView view) {
125
-        switch(eventName) {
126
-        case "CONFERENCE_WILL_JOIN":
127
-            view.setURL(eventData.getString("url"));
128
-            break;
129
-
130
-        case "CONFERENCE_FAILED":
131
-        case "CONFERENCE_WILL_LEAVE":
132
-        case "LOAD_CONFIG_ERROR":
133
-            String url = eventData.getString("url");
134
-
135
-            if (url != null && url.equals(view.getURL())) {
136
-                view.setURL(null);
137
-            }
138
-            break;
139
-        }
140
-    }
141
-
142
     /**
53
     /**
143
      * Dispatches an event that occurred on the JavaScript side of the SDK to
54
      * Dispatches an event that occurred on the JavaScript side of the SDK to
144
-     * the specified {@link JitsiMeetView}'s listener.
55
+     * the specified {@link BaseReactView}'s listener.
145
      *
56
      *
146
      * @param name The name of the event.
57
      * @param name The name of the event.
147
      * @param data The details/specifics of the event to send determined
58
      * @param data The details/specifics of the event to send determined
154
                           final String scope) {
65
                           final String scope) {
155
         // The JavaScript App needs to provide uniquely identifying information
66
         // The JavaScript App needs to provide uniquely identifying information
156
         // to the native ExternalAPI module so that the latter may match the
67
         // to the native ExternalAPI module so that the latter may match the
157
-        // former to the native JitsiMeetView which hosts it.
158
-        JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
68
+        // former to the native BaseReactView which hosts it.
69
+        BaseReactView view = BaseReactView.findViewByExternalAPIScope(scope);
159
 
70
 
160
         if (view == null) {
71
         if (view == null) {
161
             return;
72
             return;
162
         }
73
         }
163
 
74
 
164
-        // XXX The JitsiMeetView property URL was introduced in order to address
165
-        // an exception in the Picture-in-Picture functionality which arose
166
-        // because of delays related to bridging between JavaScript and Java. To
167
-        // reduce these delays do not wait for the call to be transfered to the
168
-        // UI thread.
169
-        maybeSetViewURL(name, data, view);
170
-
171
-        // Make sure JitsiMeetView's listener is invoked on the UI thread. It
172
-        // was requested by SDK consumers.
173
-        if (UiThreadUtil.isOnUiThread()) {
174
-            sendEventOnUiThread(name, data, scope);
175
-        } else {
176
-            UiThreadUtil.runOnUiThread(new Runnable() {
177
-                @Override
178
-                public void run() {
179
-                    sendEventOnUiThread(name, data, scope);
180
-                }
181
-            });
75
+        try {
76
+            view.onExternalAPIEvent(name, data);
77
+        } catch(Exception e) {
78
+            Log.e(TAG, "onExternalAPIEvent: error sending event", e);
182
         }
79
         }
183
     }
80
     }
184
 
81
 
185
-    /**
186
-     * Dispatches an event that occurred on the JavaScript side of the SDK to
187
-     * the specified {@link JitsiMeetView}'s listener on the UI thread.
188
-     *
189
-     * @param name The name of the event.
190
-     * @param data The details/specifics of the event to send determined
191
-     * by/associated with the specified {@code name}.
192
-     * @param scope
193
-     */
194
-    private void sendEventOnUiThread(final String name,
195
-                          final ReadableMap data,
196
-                          final String scope) {
197
-        // The JavaScript App needs to provide uniquely identifying information
198
-        // to the native ExternalAPI module so that the latter may match the
199
-        // former to the native JitsiMeetView which hosts it.
200
-        JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
201
-
202
-        if (view == null) {
203
-            return;
204
-        }
205
-
206
-        JitsiMeetViewListener listener = view.getListener();
207
-
208
-        if (listener == null) {
209
-            return;
210
-        }
211
-
212
-        Method method = JITSI_MEET_VIEW_LISTENER_METHODS.get(name);
213
-
214
-        if (method != null) {
215
-            try {
216
-                method.invoke(listener, toHashMap(data));
217
-            } catch (IllegalAccessException e) {
218
-                // FIXME There was a multicatch for IllegalAccessException and
219
-                // InvocationTargetException, but Android Studio complained
220
-                // with: "Multi-catch with these reflection exceptions requires
221
-                // API level 19 (current min is 16) because they get compiled to
222
-                // the common but new super type ReflectiveOperationException.
223
-                // As a workaround either create individual catch statements, or
224
-                // catch Exception."
225
-                throw new RuntimeException(e);
226
-            } catch (InvocationTargetException e) {
227
-                throw new RuntimeException(e);
228
-            }
229
-        }
230
-    }
231
-
232
-    /**
233
-     * Initializes a new {@code HashMap} instance with the key-value
234
-     * associations of a specific {@code ReadableMap}.
235
-     *
236
-     * @param readableMap the {@code ReadableMap} specifying the key-value
237
-     * associations with which the new {@code HashMap} instance is to be
238
-     * initialized.
239
-     * @return a new {@code HashMap} instance initialized with the key-value
240
-     * associations of the specified {@code readableMap}.
241
-     */
242
-    private HashMap<String, Object> toHashMap(ReadableMap readableMap) {
243
-        HashMap<String, Object> hashMap = new HashMap<>();
244
-
245
-        for (ReadableMapKeySetIterator i = readableMap.keySetIterator();
246
-                i.hasNextKey();) {
247
-            String key = i.nextKey();
248
-
249
-            hashMap.put(key, readableMap.getString(key));
250
-        }
251
-
252
-        return hashMap;
253
-    }
254
 }
82
 }

+ 19
- 6
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java View File

177
 
177
 
178
     @Override
178
     @Override
179
     public void onBackPressed() {
179
     public void onBackPressed() {
180
-        if (!JitsiMeetView.onBackPressed()) {
180
+        if (!ReactActivityLifecycleAdapter.onBackPressed()) {
181
             // JitsiMeetView didn't handle the invocation of the back button.
181
             // JitsiMeetView didn't handle the invocation of the back button.
182
             // Generally, an Activity extender would very likely want to invoke
182
             // Generally, an Activity extender would very likely want to invoke
183
             // Activity#onBackPressed(). For the sake of consistency with
183
             // Activity#onBackPressed(). For the sake of consistency with
220
             view = null;
220
             view = null;
221
         }
221
         }
222
 
222
 
223
-        JitsiMeetView.onHostDestroy(this);
223
+        ReactActivityLifecycleAdapter.onHostDestroy(this);
224
     }
224
     }
225
 
225
 
226
     // ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java
226
     // ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java
242
 
242
 
243
     @Override
243
     @Override
244
     public void onNewIntent(Intent intent) {
244
     public void onNewIntent(Intent intent) {
245
-        JitsiMeetView.onNewIntent(intent);
245
+        // XXX At least twice we received bug reports about malfunctioning
246
+        // loadURL in the Jitsi Meet SDK while the Jitsi Meet app seemed to
247
+        // functioning as expected in our testing. But that was to be expected
248
+        // because the app does not exercise loadURL. In order to increase the
249
+        // test coverage of loadURL, channel deep linking through loadURL.
250
+        Uri uri;
251
+
252
+        if (Intent.ACTION_VIEW.equals(intent.getAction())
253
+                && (uri = intent.getData()) != null
254
+                && JitsiMeetView.loadURLStringInViews(uri.toString())) {
255
+            return;
256
+        }
257
+
258
+        ReactActivityLifecycleAdapter.onNewIntent(intent);
246
     }
259
     }
247
 
260
 
248
     @Override
261
     @Override
250
         super.onResume();
263
         super.onResume();
251
 
264
 
252
         defaultBackButtonImpl = new DefaultHardwareBackBtnHandlerImpl(this);
265
         defaultBackButtonImpl = new DefaultHardwareBackBtnHandlerImpl(this);
253
-        JitsiMeetView.onHostResume(this, defaultBackButtonImpl);
266
+        ReactActivityLifecycleAdapter.onHostResume(this, defaultBackButtonImpl);
254
     }
267
     }
255
 
268
 
256
     @Override
269
     @Override
257
     public void onStop() {
270
     public void onStop() {
258
         super.onStop();
271
         super.onStop();
259
 
272
 
260
-        JitsiMeetView.onHostPause(this);
273
+        ReactActivityLifecycleAdapter.onHostPause(this);
261
         defaultBackButtonImpl = null;
274
         defaultBackButtonImpl = null;
262
     }
275
     }
263
 
276
 
264
     @Override
277
     @Override
265
     protected void onUserLeaveHint() {
278
     protected void onUserLeaveHint() {
266
         if (view != null) {
279
         if (view != null) {
267
-            view.onUserLeaveHint();
280
+            view.enterPictureInPicture();
268
         }
281
         }
269
     }
282
     }
270
 
283
 

+ 75
- 252
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java View File

16
 
16
 
17
 package org.jitsi.meet.sdk;
17
 package org.jitsi.meet.sdk;
18
 
18
 
19
-import android.app.Activity;
20
 import android.content.Context;
19
 import android.content.Context;
21
-import android.content.Intent;
22
-import android.net.Uri;
23
 import android.os.Bundle;
20
 import android.os.Bundle;
24
 import android.support.annotation.NonNull;
21
 import android.support.annotation.NonNull;
25
 import android.support.annotation.Nullable;
22
 import android.support.annotation.Nullable;
26
 import android.util.Log;
23
 import android.util.Log;
27
-import android.widget.FrameLayout;
28
 
24
 
29
-import com.facebook.react.ReactInstanceManager;
30
-import com.facebook.react.ReactRootView;
31
-import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
32
-import com.rnimmersive.RNImmersiveModule;
25
+import com.facebook.react.bridge.ReadableMap;
33
 
26
 
34
 import org.jitsi.meet.sdk.invite.InviteController;
27
 import org.jitsi.meet.sdk.invite.InviteController;
35
 
28
 
29
+import java.lang.reflect.Method;
36
 import java.net.URL;
30
 import java.net.URL;
37
-import java.util.Collections;
38
-import java.util.Set;
39
-import java.util.UUID;
40
-import java.util.WeakHashMap;
31
+import java.util.Map;
41
 
32
 
42
-public class JitsiMeetView extends FrameLayout {
33
+public class JitsiMeetView extends BaseReactView {
43
     /**
34
     /**
44
-     * Background color used by {@code JitsiMeetView} and the React Native root
45
-     * view.
35
+     * The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
36
+     * redux action types.
46
      */
37
      */
47
-    private static final int BACKGROUND_COLOR = 0xFF111111;
38
+    private static final Map<String, Method> LISTENER_METHODS
39
+        = ListenerUtils.slurpListenerMethods(JitsiMeetViewListener.class);
48
 
40
 
49
     /**
41
     /**
50
      * The {@link Log} tag which identifies the source of the log messages of
42
      * The {@link Log} tag which identifies the source of the log messages of
51
      * {@code JitsiMeetView}.
43
      * {@code JitsiMeetView}.
52
      */
44
      */
53
-    private final static String TAG = JitsiMeetView.class.getSimpleName();
54
-
55
-    private static final Set<JitsiMeetView> views
56
-        = Collections.newSetFromMap(new WeakHashMap<JitsiMeetView, Boolean>());
57
-
58
-    public static JitsiMeetView findViewByExternalAPIScope(
59
-            String externalAPIScope) {
60
-        synchronized (views) {
61
-            for (JitsiMeetView view : views) {
62
-                if (view.externalAPIScope.equals(externalAPIScope)) {
63
-                    return view;
64
-                }
65
-            }
66
-        }
67
-
68
-        return null;
69
-    }
45
+    private static final String TAG = JitsiMeetView.class.getSimpleName();
70
 
46
 
71
     /**
47
     /**
72
      * Loads a specific URL {@code String} in all existing
48
      * Loads a specific URL {@code String} in all existing
78
      * at least one {@code JitsiMeetView}, then {@code true}; otherwise,
54
      * at least one {@code JitsiMeetView}, then {@code true}; otherwise,
79
      * {@code false}.
55
      * {@code false}.
80
      */
56
      */
81
-    private static boolean loadURLStringInViews(String urlString) {
57
+    public static boolean loadURLStringInViews(String urlString) {
58
+        boolean loaded = false;
59
+
82
         synchronized (views) {
60
         synchronized (views) {
83
-            if (!views.isEmpty()) {
84
-                for (JitsiMeetView view : views) {
85
-                    view.loadURLString(urlString);
61
+            for (BaseReactView view : views) {
62
+                if (view instanceof JitsiMeetView) {
63
+                    ((JitsiMeetView)view).loadURLString(urlString);
64
+                    loaded = true;
86
                 }
65
                 }
87
-
88
-                return true;
89
             }
66
             }
90
         }
67
         }
91
 
68
 
92
-        return false;
93
-    }
94
-
95
-    /**
96
-     * Activity lifecycle method which should be called from
97
-     * {@code Activity.onBackPressed} so we can do the required internal
98
-     * processing.
99
-     *
100
-     * @return {@code true} if the back-press was processed; {@code false},
101
-     * otherwise. If {@code false}, the application should call the parent's
102
-     * implementation.
103
-     */
104
-    public static boolean onBackPressed() {
105
-        ReactInstanceManager reactInstanceManager
106
-            = ReactInstanceManagerHolder.getReactInstanceManager();
107
-
108
-        if (reactInstanceManager == null) {
109
-            return false;
110
-        } else {
111
-            reactInstanceManager.onBackPressed();
112
-            return true;
113
-        }
114
-    }
115
-
116
-    /**
117
-     * Activity lifecycle method which should be called from
118
-     * {@code Activity.onDestroy} so we can do the required internal
119
-     * processing.
120
-     *
121
-     * @param activity {@code Activity} being destroyed.
122
-     */
123
-    public static void onHostDestroy(Activity activity) {
124
-        ReactInstanceManager reactInstanceManager
125
-            = ReactInstanceManagerHolder.getReactInstanceManager();
126
-
127
-        if (reactInstanceManager != null) {
128
-            reactInstanceManager.onHostDestroy(activity);
129
-        }
130
-    }
131
-
132
-    /**
133
-     * Activity lifecycle method which should be called from
134
-     * {@code Activity.onPause} so we can do the required internal processing.
135
-     *
136
-     * @param activity {@code Activity} being paused.
137
-     */
138
-    public static void onHostPause(Activity activity) {
139
-        ReactInstanceManager reactInstanceManager
140
-            = ReactInstanceManagerHolder.getReactInstanceManager();
141
-
142
-        if (reactInstanceManager != null) {
143
-            reactInstanceManager.onHostPause(activity);
144
-        }
145
-    }
146
-
147
-    /**
148
-     * Activity lifecycle method which should be called from
149
-     * {@code Activity.onResume} so we can do the required internal processing.
150
-     *
151
-     * @param activity {@code Activity} being resumed.
152
-     */
153
-    public static void onHostResume(Activity activity) {
154
-        onHostResume(activity, new DefaultHardwareBackBtnHandlerImpl(activity));
155
-    }
156
-
157
-    /**
158
-     * Activity lifecycle method which should be called from
159
-     * {@code Activity.onResume} so we can do the required internal processing.
160
-     *
161
-     * @param activity {@code Activity} being resumed.
162
-     * @param defaultBackButtonImpl a {@code DefaultHardwareBackBtnHandler} to
163
-     * handle invoking the back button if no {@code JitsiMeetView} handles it.
164
-     */
165
-    public static void onHostResume(
166
-            Activity activity,
167
-            DefaultHardwareBackBtnHandler defaultBackButtonImpl) {
168
-        ReactInstanceManager reactInstanceManager
169
-            = ReactInstanceManagerHolder.getReactInstanceManager();
170
-
171
-        if (reactInstanceManager != null) {
172
-            reactInstanceManager.onHostResume(activity, defaultBackButtonImpl);
173
-        }
174
-    }
175
-
176
-    /**
177
-     * Activity lifecycle method which should be called from
178
-     * {@code Activity.onNewIntent} so we can do the required internal
179
-     * processing. Note that this is only needed if the activity's "launchMode"
180
-     * was set to "singleTask". This is required for deep linking to work once
181
-     * the application is already running.
182
-     *
183
-     * @param intent {@code Intent} instance which was received.
184
-     */
185
-    public static void onNewIntent(Intent intent) {
186
-        // XXX At least twice we received bug reports about malfunctioning
187
-        // loadURL in the Jitsi Meet SDK while the Jitsi Meet app seemed to
188
-        // functioning as expected in our testing. But that was to be expected
189
-        // because the app does not exercise loadURL. In order to increase the
190
-        // test coverage of loadURL, channel deep linking through loadURL.
191
-        Uri uri;
192
-
193
-        if (Intent.ACTION_VIEW.equals(intent.getAction())
194
-                && (uri = intent.getData()) != null
195
-                && loadURLStringInViews(uri.toString())) {
196
-            return;
197
-        }
198
-
199
-        ReactInstanceManager reactInstanceManager
200
-            = ReactInstanceManagerHolder.getReactInstanceManager();
201
-
202
-        if (reactInstanceManager != null) {
203
-            reactInstanceManager.onNewIntent(intent);
204
-        }
69
+        return loaded;
205
     }
70
     }
206
 
71
 
207
     /**
72
     /**
211
      */
76
      */
212
     private URL defaultURL;
77
     private URL defaultURL;
213
 
78
 
214
-    /**
215
-     * The unique identifier of this {@code JitsiMeetView} within the process
216
-     * for the purposes of {@link ExternalAPI}. The name scope was inspired by
217
-     * postis which we use on Web for the similar purposes of the iframe-based
218
-     * external API.
219
-     */
220
-    private final String externalAPIScope;
221
-
222
     /**
79
     /**
223
      * The entry point into the invite feature of Jitsi Meet. The Java
80
      * The entry point into the invite feature of Jitsi Meet. The Java
224
      * counterpart of the JavaScript {@code InviteButton}.
81
      * counterpart of the JavaScript {@code InviteButton}.
238
      */
95
      */
239
     private Boolean pictureInPictureEnabled;
96
     private Boolean pictureInPictureEnabled;
240
 
97
 
241
-    /**
242
-     * React Native root view.
243
-     */
244
-    private ReactRootView reactRootView;
245
-
246
     /**
98
     /**
247
      * The URL of the current conference.
99
      * The URL of the current conference.
248
      */
100
      */
258
     public JitsiMeetView(@NonNull Context context) {
110
     public JitsiMeetView(@NonNull Context context) {
259
         super(context);
111
         super(context);
260
 
112
 
261
-        setBackgroundColor(BACKGROUND_COLOR);
262
-
263
-        ReactInstanceManagerHolder.initReactInstanceManager(
264
-            ((Activity) context).getApplication());
265
-
266
-        // Hook this JitsiMeetView into ExternalAPI.
267
-        externalAPIScope = UUID.randomUUID().toString();
268
-        synchronized (views) {
269
-            views.add(this);
270
-        }
271
-
272
         // The entry point into the invite feature of Jitsi Meet. The Java
113
         // The entry point into the invite feature of Jitsi Meet. The Java
273
         // counterpart of the JavaScript InviteButton.
114
         // counterpart of the JavaScript InviteButton.
274
         inviteController = new InviteController(externalAPIScope);
115
         inviteController = new InviteController(externalAPIScope);
275
     }
116
     }
276
 
117
 
277
     /**
118
     /**
278
-     * Releases the React resources (specifically the {@link ReactRootView})
279
-     * associated with this view.
119
+     * Enters Picture-In-Picture mode, if possible. This method is designed to
120
+     * be called from the {@code Activity.onUserLeaveHint} method.
280
      *
121
      *
281
-     * This method MUST be called when the Activity holding this view is
282
-     * destroyed, typically in the {@code onDestroy} method.
122
+     * This is currently not mandatory, but if used will provide automatic
123
+     * handling of the picture in picture mode when user minimizes the app. It
124
+     * will be probably the most useful in case the app is using the welcome
125
+     * page.
283
      */
126
      */
284
-    public void dispose() {
285
-        if (reactRootView != null) {
286
-            removeView(reactRootView);
287
-            reactRootView.unmountReactApplication();
288
-            reactRootView = null;
127
+    public void enterPictureInPicture() {
128
+        if (getPictureInPictureEnabled() && getURL() != null) {
129
+            PictureInPictureModule pipModule
130
+                = ReactInstanceManagerHolder.getNativeModule(
131
+                PictureInPictureModule.class);
132
+
133
+            if (pipModule != null) {
134
+                try {
135
+                    pipModule.enterPictureInPicture();
136
+                } catch (RuntimeException re) {
137
+                    Log.e(TAG, "onUserLeaveHint: failed to enter PiP mode", re);
138
+                }
139
+            }
289
         }
140
         }
290
     }
141
     }
291
 
142
 
294
      * partial URL (e.g. a room name only) is specified to
145
      * partial URL (e.g. a room name only) is specified to
295
      * {@link #loadURLString(String)} or {@link #loadURLObject(Bundle)}. If not
146
      * {@link #loadURLString(String)} or {@link #loadURLObject(Bundle)}. If not
296
      * set or if set to {@code null}, the default built in JavaScript is used:
147
      * set or if set to {@code null}, the default built in JavaScript is used:
297
-     * {@link https://meet.jit.si}
148
+     * https://meet.jit.si
298
      *
149
      *
299
      * @return The default base {@code URL} or {@code null}.
150
      * @return The default base {@code URL} or {@code null}.
300
      */
151
      */
337
         return
188
         return
338
             PictureInPictureModule.isPictureInPictureSupported()
189
             PictureInPictureModule.isPictureInPictureSupported()
339
                 && (pictureInPictureEnabled == null
190
                 && (pictureInPictureEnabled == null
340
-                    || pictureInPictureEnabled.booleanValue());
191
+                    || pictureInPictureEnabled);
341
     }
192
     }
342
 
193
 
343
     /**
194
     /**
395
             props.putString("defaultURL", defaultURL.toString());
246
             props.putString("defaultURL", defaultURL.toString());
396
         }
247
         }
397
 
248
 
398
-        // externalAPIScope
399
-        props.putString("externalAPIScope", externalAPIScope);
400
-
401
         // inviteController
249
         // inviteController
402
         InviteController inviteController = getInviteController();
250
         InviteController inviteController = getInviteController();
403
 
251
 
434
         // per loadURLObject: invocation.
282
         // per loadURLObject: invocation.
435
         props.putLong("timestamp", System.currentTimeMillis());
283
         props.putLong("timestamp", System.currentTimeMillis());
436
 
284
 
437
-        if (reactRootView == null) {
438
-            reactRootView = new ReactRootView(getContext());
439
-            reactRootView.startReactApplication(
440
-                ReactInstanceManagerHolder.getReactInstanceManager(),
441
-                "App",
442
-                props);
443
-            reactRootView.setBackgroundColor(BACKGROUND_COLOR);
444
-            addView(reactRootView);
445
-        } else {
446
-            reactRootView.setAppProperties(props);
447
-        }
285
+        createReactRootView("App", props);
448
     }
286
     }
449
 
287
 
450
     /**
288
     /**
468
     }
306
     }
469
 
307
 
470
     /**
308
     /**
471
-     * Activity lifecycle method which should be called from
472
-     * {@code Activity.onUserLeaveHint} so we can do the required internal
473
-     * processing.
309
+     * The internal processing for the URL of the current conference set on the
310
+     * associated {@link JitsiMeetView}.
474
      *
311
      *
475
-     * This is currently not mandatory, but if used will provide automatic
476
-     * handling of the picture in picture mode when user minimizes the app. It
477
-     * will be probably the most useful in case the app is using the welcome
478
-     * page.
479
-     */
480
-    public void onUserLeaveHint() {
481
-        if (getPictureInPictureEnabled() && getURL() != null) {
482
-            PictureInPictureModule pipModule
483
-                = ReactInstanceManagerHolder.getNativeModule(
484
-                        PictureInPictureModule.class);
485
-
486
-            if (pipModule != null) {
487
-                try {
488
-                    pipModule.enterPictureInPicture();
489
-                } catch (RuntimeException re) {
490
-                    Log.e(TAG, "onUserLeaveHint: failed to enter PiP mode", re);
491
-                }
312
+     * @param eventName the name of the external API event to be processed
313
+     * @param eventData the details/specifics of the event to process determined
314
+     * by/associated with the specified {@code eventName}.
315
+     */
316
+    private void maybeSetViewURL(String eventName, ReadableMap eventData) {
317
+        switch(eventName) {
318
+        case "CONFERENCE_WILL_JOIN":
319
+            setURL(eventData.getString("url"));
320
+            break;
321
+
322
+        case "CONFERENCE_FAILED":
323
+        case "CONFERENCE_WILL_LEAVE":
324
+        case "LOAD_CONFIG_ERROR":
325
+            String url = eventData.getString("url");
326
+
327
+            if (url != null && url.equals(getURL())) {
328
+                setURL(null);
492
             }
329
             }
330
+            break;
493
         }
331
         }
494
     }
332
     }
495
 
333
 
496
     /**
334
     /**
497
-     * Called when the window containing this view gains or loses focus.
335
+     * Handler for {@link ExternalAPIModule} events.
498
      *
336
      *
499
-     * @param hasFocus If the window of this view now has focus, {@code true};
500
-     * otherwise, {@code false}.
337
+     * @param name - Name of the event.
338
+     * @param data - Event data.
501
      */
339
      */
502
     @Override
340
     @Override
503
-    public void onWindowFocusChanged(boolean hasFocus) {
504
-        super.onWindowFocusChanged(hasFocus);
505
-
506
-        // https://github.com/mockingbot/react-native-immersive#restore-immersive-state
507
-
508
-        // FIXME The singleton pattern employed by RNImmersiveModule is not
509
-        // advisable because a react-native mobule is consumable only after its
510
-        // BaseJavaModule#initialize() has completed and here we have no
511
-        // knowledge of whether the precondition is really met.
512
-        RNImmersiveModule immersive = RNImmersiveModule.getInstance();
513
-
514
-        if (hasFocus && immersive != null) {
515
-            try {
516
-                immersive.emitImmersiveStateChangeEvent();
517
-            } catch (RuntimeException re) {
518
-                // FIXME I don't know how to check myself whether
519
-                // BaseJavaModule#initialize() has been invoked and thus
520
-                // RNImmersiveModule is consumable. A safe workaround is to
521
-                // swallow the failure because the whole full-screen/immersive
522
-                // functionality is brittle anyway, akin to the icing on the
523
-                // cake, and has been working without onWindowFocusChanged for a
524
-                // very long time.
525
-                Log.e(
526
-                    TAG,
527
-                    "RNImmersiveModule#emitImmersiveStateChangeEvent() failed!",
528
-                    re);
529
-            }
341
+    public void onExternalAPIEvent(String name, ReadableMap data) {
342
+        // XXX The JitsiMeetView property URL was introduced in order to address
343
+        // an exception in the Picture-in-Picture functionality which arose
344
+        // because of delays related to bridging between JavaScript and Java. To
345
+        // reduce these delays do not wait for the call to be transferred to the
346
+        // UI thread.
347
+        maybeSetViewURL(name, data);
348
+
349
+        JitsiMeetViewListener listener = getListener();
350
+        if (listener != null) {
351
+            ListenerUtils.runListenerMethod(
352
+                listener, LISTENER_METHODS, name, data);
530
         }
353
         }
531
     }
354
     }
532
 
355
 
563
      * {@code true}; otherwise, {@code false}.
386
      * {@code true}; otherwise, {@code false}.
564
      */
387
      */
565
     public void setPictureInPictureEnabled(boolean pictureInPictureEnabled) {
388
     public void setPictureInPictureEnabled(boolean pictureInPictureEnabled) {
566
-        this.pictureInPictureEnabled = Boolean.valueOf(pictureInPictureEnabled);
389
+        this.pictureInPictureEnabled = pictureInPictureEnabled;
567
     }
390
     }
568
 
391
 
569
     /**
392
     /**

+ 150
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/ListenerUtils.java View File

1
+package org.jitsi.meet.sdk;
2
+
3
+import com.facebook.react.bridge.ReadableMap;
4
+import com.facebook.react.bridge.ReadableMapKeySetIterator;
5
+import com.facebook.react.bridge.UiThreadUtil;
6
+
7
+import java.lang.reflect.InvocationTargetException;
8
+import java.lang.reflect.Method;
9
+import java.lang.reflect.Modifier;
10
+import java.util.HashMap;
11
+import java.util.Locale;
12
+import java.util.Map;
13
+import java.util.regex.Pattern;
14
+
15
+/**
16
+ * Utility methods for helping with transforming {@link ExternalAPIModule}
17
+ * events into listener methods. Used with descendants of {@link BaseReactView}.
18
+ */
19
+public final class ListenerUtils {
20
+    /**
21
+     * Extracts the methods defined in a listener and creates a mapping of this
22
+     * form: event name -> method.
23
+     *
24
+     * @param listener - The listener whose methods we want to slurp.
25
+     * @return A mapping with event names - methods.
26
+     */
27
+    public static Map<String, Method> slurpListenerMethods(Class listener) {
28
+        final Map<String, Method> methods = new HashMap<>();
29
+
30
+        // Figure out the mapping between the listener methods
31
+        // and the events i.e. redux action types.
32
+        Pattern onPattern = Pattern.compile("^on[A-Z]+");
33
+        Pattern camelcasePattern = Pattern.compile("([a-z0-9]+)([A-Z0-9]+)");
34
+
35
+        for (Method method : listener.getDeclaredMethods()) {
36
+            // * The method must be public (because it is declared by an
37
+            //   interface).
38
+            // * The method must be/return void.
39
+            if (!Modifier.isPublic(method.getModifiers())
40
+                    || !Void.TYPE.equals(method.getReturnType())) {
41
+                continue;
42
+            }
43
+
44
+            // * The method name must start with "on" followed by a
45
+            //   capital/uppercase letter (in agreement with the camelcase
46
+            //   coding style customary to Java in general and the projects of
47
+            //   the Jitsi community in particular).
48
+            String name = method.getName();
49
+
50
+            if (!onPattern.matcher(name).find()) {
51
+                continue;
52
+            }
53
+
54
+            // * The method must accept/have exactly 1 parameter of a type
55
+            //   assignable from HashMap.
56
+            Class<?>[] parameterTypes = method.getParameterTypes();
57
+
58
+            if (parameterTypes.length != 1
59
+                    || !parameterTypes[0].isAssignableFrom(HashMap.class)) {
60
+                continue;
61
+            }
62
+
63
+            // Convert the method name to an event name.
64
+            name
65
+                = camelcasePattern.matcher(name.substring(2))
66
+                    .replaceAll("$1_$2")
67
+                    .toUpperCase(Locale.ROOT);
68
+            methods.put(name, method);
69
+        }
70
+
71
+        return methods;
72
+    }
73
+
74
+    /**
75
+     * Executes the right listener method for the given event.
76
+     * NOTE: This function will run asynchronously on the UI thread.
77
+     *
78
+     * @param listener - The listener on which the method will be called.
79
+     * @param listenerMethods - Mapping with event names and the matching
80
+     *                        methods.
81
+     * @param eventName - Name of the event.
82
+     * @param eventData - Data associated with the event.
83
+     */
84
+    public static void runListenerMethod(
85
+            final Object listener,
86
+            final Map<String, Method> listenerMethods,
87
+            final String eventName,
88
+            final ReadableMap eventData) {
89
+        // Make sure listener methods are invoked on the UI thread. It
90
+        // was requested by SDK consumers.
91
+        if (UiThreadUtil.isOnUiThread()) {
92
+            runListenerMethodOnUiThread(
93
+                listener, listenerMethods, eventName, eventData);
94
+        } else {
95
+            UiThreadUtil.runOnUiThread(new Runnable() {
96
+                @Override
97
+                public void run() {
98
+                    runListenerMethodOnUiThread(
99
+                        listener, listenerMethods, eventName, eventData);
100
+                }
101
+            });
102
+        }
103
+    }
104
+
105
+    /**
106
+     * Helper companion for {@link ListenerUtils#runListenerMethod} which runs
107
+     * in the UI thread.
108
+     */
109
+    private static void runListenerMethodOnUiThread(
110
+            Object listener,
111
+            Map<String, Method> listenerMethods,
112
+            String eventName,
113
+            ReadableMap eventData) {
114
+        UiThreadUtil.assertOnUiThread();
115
+
116
+        Method method = listenerMethods.get(eventName);
117
+        if (method != null) {
118
+            try {
119
+                method.invoke(listener, toHashMap(eventData));
120
+            } catch (IllegalAccessException e) {
121
+                throw new RuntimeException(e);
122
+            } catch (InvocationTargetException e) {
123
+                throw new RuntimeException(e);
124
+            }
125
+        }
126
+    }
127
+
128
+    /**
129
+     * Initializes a new {@code HashMap} instance with the key-value
130
+     * associations of a specific {@code ReadableMap}.
131
+     *
132
+     * @param readableMap the {@code ReadableMap} specifying the key-value
133
+     * associations with which the new {@code HashMap} instance is to be
134
+     * initialized.
135
+     * @return a new {@code HashMap} instance initialized with the key-value
136
+     * associations of the specified {@code readableMap}.
137
+     */
138
+    private static HashMap<String, Object> toHashMap(ReadableMap readableMap) {
139
+        HashMap<String, Object> hashMap = new HashMap<>();
140
+
141
+        for (ReadableMapKeySetIterator i = readableMap.keySetIterator();
142
+                i.hasNextKey();) {
143
+            String key = i.nextKey();
144
+
145
+            hashMap.put(key, readableMap.getString(key));
146
+        }
147
+
148
+        return hashMap;
149
+    }
150
+}

+ 113
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/ReactActivityLifecycleAdapter.java View File

1
+package org.jitsi.meet.sdk;
2
+
3
+import android.app.Activity;
4
+import android.content.Intent;
5
+
6
+import com.facebook.react.ReactInstanceManager;
7
+import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
8
+
9
+/**
10
+ * Helper class to encapsulate the work which needs to be done on Activity
11
+ * lifecycle methods in order for the React side to be aware of it.
12
+ */
13
+class ReactActivityLifecycleAdapter {
14
+    /**
15
+     * Activity lifecycle method which should be called from
16
+     * {@code Activity.onBackPressed} so we can do the required internal
17
+     * processing.
18
+     *
19
+     * @return {@code true} if the back-press was processed; {@code false},
20
+     * otherwise. If {@code false}, the application should call the parent's
21
+     * implementation.
22
+     */
23
+    public static boolean onBackPressed() {
24
+        ReactInstanceManager reactInstanceManager
25
+            = ReactInstanceManagerHolder.getReactInstanceManager();
26
+
27
+        if (reactInstanceManager == null) {
28
+            return false;
29
+        } else {
30
+            reactInstanceManager.onBackPressed();
31
+            return true;
32
+        }
33
+    }
34
+
35
+    /**
36
+     * Activity lifecycle method which should be called from
37
+     * {@code Activity.onDestroy} so we can do the required internal
38
+     * processing.
39
+     *
40
+     * @param activity {@code Activity} being destroyed.
41
+     */
42
+    public static void onHostDestroy(Activity activity) {
43
+        ReactInstanceManager reactInstanceManager
44
+            = ReactInstanceManagerHolder.getReactInstanceManager();
45
+
46
+        if (reactInstanceManager != null) {
47
+            reactInstanceManager.onHostDestroy(activity);
48
+        }
49
+    }
50
+
51
+    /**
52
+     * Activity lifecycle method which should be called from
53
+     * {@code Activity.onPause} so we can do the required internal processing.
54
+     *
55
+     * @param activity {@code Activity} being paused.
56
+     */
57
+    public static void onHostPause(Activity activity) {
58
+        ReactInstanceManager reactInstanceManager
59
+            = ReactInstanceManagerHolder.getReactInstanceManager();
60
+
61
+        if (reactInstanceManager != null) {
62
+            reactInstanceManager.onHostPause(activity);
63
+        }
64
+    }
65
+
66
+    /**
67
+     * Activity lifecycle method which should be called from
68
+     * {@code Activity.onResume} so we can do the required internal processing.
69
+     *
70
+     * @param activity {@code Activity} being resumed.
71
+     */
72
+    public static void onHostResume(Activity activity) {
73
+        onHostResume(activity, new DefaultHardwareBackBtnHandlerImpl(activity));
74
+    }
75
+
76
+    /**
77
+     * Activity lifecycle method which should be called from
78
+     * {@code Activity.onResume} so we can do the required internal processing.
79
+     *
80
+     * @param activity {@code Activity} being resumed.
81
+     * @param defaultBackButtonImpl a {@code DefaultHardwareBackBtnHandler} to
82
+     * handle invoking the back button if no {@code JitsiMeetView} handles it.
83
+     */
84
+    public static void onHostResume(
85
+        Activity activity,
86
+        DefaultHardwareBackBtnHandler defaultBackButtonImpl) {
87
+        ReactInstanceManager reactInstanceManager
88
+            = ReactInstanceManagerHolder.getReactInstanceManager();
89
+
90
+        if (reactInstanceManager != null) {
91
+            reactInstanceManager.onHostResume(activity, defaultBackButtonImpl);
92
+        }
93
+    }
94
+
95
+    /**
96
+     * Activity lifecycle method which should be called from
97
+     * {@code Activity.onNewIntent} so we can do the required internal
98
+     * processing. Note that this is only needed if the activity's "launchMode"
99
+     * was set to "singleTask". This is required for deep linking to work once
100
+     * the application is already running.
101
+     *
102
+     * @param intent {@code Intent} instance which was received.
103
+     */
104
+    public static void onNewIntent(Intent intent) {
105
+        ReactInstanceManager reactInstanceManager
106
+            = ReactInstanceManagerHolder.getReactInstanceManager();
107
+
108
+        if (reactInstanceManager != null) {
109
+            reactInstanceManager.onNewIntent(intent);
110
+        }
111
+    }
112
+
113
+}

+ 2
- 1
android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteModule.java View File

24
 import com.facebook.react.bridge.ReadableArray;
24
 import com.facebook.react.bridge.ReadableArray;
25
 import com.facebook.react.bridge.UiThreadUtil;
25
 import com.facebook.react.bridge.UiThreadUtil;
26
 
26
 
27
+import org.jitsi.meet.sdk.BaseReactView;
27
 import org.jitsi.meet.sdk.JitsiMeetView;
28
 import org.jitsi.meet.sdk.JitsiMeetView;
28
 
29
 
29
 /**
30
 /**
67
     private InviteController findInviteControllerByExternalAPIScope(
68
     private InviteController findInviteControllerByExternalAPIScope(
68
             String externalAPIScope) {
69
             String externalAPIScope) {
69
         JitsiMeetView view
70
         JitsiMeetView view
70
-            = JitsiMeetView.findViewByExternalAPIScope(externalAPIScope);
71
+            = (JitsiMeetView)BaseReactView.findViewByExternalAPIScope(externalAPIScope);
71
 
72
 
72
         return view == null ? null : view.getInviteController();
73
         return view == null ? null : view.getInviteController();
73
     }
74
     }

Loading…
Cancel
Save