Explorar el Código

[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é hace 6 años
padre
commit
9972e88b67

+ 171
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/BaseReactView.java Ver fichero

@@ -0,0 +1,171 @@
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 Ver fichero

@@ -16,77 +16,18 @@
16 16
 
17 17
 package org.jitsi.meet.sdk;
18 18
 
19
+import android.util.Log;
20
+
19 21
 import com.facebook.react.bridge.ReactApplicationContext;
20 22
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
21 23
 import com.facebook.react.bridge.ReactMethod;
22 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 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 33
      * Initializes a new module instance. There shall be a single instance of
@@ -109,39 +50,9 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
109 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 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 57
      * @param name The name of the event.
147 58
      * @param data The details/specifics of the event to send determined
@@ -154,101 +65,18 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
154 65
                           final String scope) {
155 66
         // The JavaScript App needs to provide uniquely identifying information
156 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 71
         if (view == null) {
161 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 Ver fichero

@@ -177,7 +177,7 @@ public class JitsiMeetActivity extends AppCompatActivity {
177 177
 
178 178
     @Override
179 179
     public void onBackPressed() {
180
-        if (!JitsiMeetView.onBackPressed()) {
180
+        if (!ReactActivityLifecycleAdapter.onBackPressed()) {
181 181
             // JitsiMeetView didn't handle the invocation of the back button.
182 182
             // Generally, an Activity extender would very likely want to invoke
183 183
             // Activity#onBackPressed(). For the sake of consistency with
@@ -220,7 +220,7 @@ public class JitsiMeetActivity extends AppCompatActivity {
220 220
             view = null;
221 221
         }
222 222
 
223
-        JitsiMeetView.onHostDestroy(this);
223
+        ReactActivityLifecycleAdapter.onHostDestroy(this);
224 224
     }
225 225
 
226 226
     // ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java
@@ -242,7 +242,20 @@ public class JitsiMeetActivity extends AppCompatActivity {
242 242
 
243 243
     @Override
244 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 261
     @Override
@@ -250,21 +263,21 @@ public class JitsiMeetActivity extends AppCompatActivity {
250 263
         super.onResume();
251 264
 
252 265
         defaultBackButtonImpl = new DefaultHardwareBackBtnHandlerImpl(this);
253
-        JitsiMeetView.onHostResume(this, defaultBackButtonImpl);
266
+        ReactActivityLifecycleAdapter.onHostResume(this, defaultBackButtonImpl);
254 267
     }
255 268
 
256 269
     @Override
257 270
     public void onStop() {
258 271
         super.onStop();
259 272
 
260
-        JitsiMeetView.onHostPause(this);
273
+        ReactActivityLifecycleAdapter.onHostPause(this);
261 274
         defaultBackButtonImpl = null;
262 275
     }
263 276
 
264 277
     @Override
265 278
     protected void onUserLeaveHint() {
266 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 Ver fichero

@@ -16,57 +16,33 @@
16 16
 
17 17
 package org.jitsi.meet.sdk;
18 18
 
19
-import android.app.Activity;
20 19
 import android.content.Context;
21
-import android.content.Intent;
22
-import android.net.Uri;
23 20
 import android.os.Bundle;
24 21
 import android.support.annotation.NonNull;
25 22
 import android.support.annotation.Nullable;
26 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 27
 import org.jitsi.meet.sdk.invite.InviteController;
35 28
 
29
+import java.lang.reflect.Method;
36 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 42
      * The {@link Log} tag which identifies the source of the log messages of
51 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 48
      * Loads a specific URL {@code String} in all existing
@@ -78,130 +54,19 @@ public class JitsiMeetView extends FrameLayout {
78 54
      * at least one {@code JitsiMeetView}, then {@code true}; otherwise,
79 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 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,14 +76,6 @@ public class JitsiMeetView extends FrameLayout {
211 76
      */
212 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 80
      * The entry point into the invite feature of Jitsi Meet. The Java
224 81
      * counterpart of the JavaScript {@code InviteButton}.
@@ -238,11 +95,6 @@ public class JitsiMeetView extends FrameLayout {
238 95
      */
239 96
     private Boolean pictureInPictureEnabled;
240 97
 
241
-    /**
242
-     * React Native root view.
243
-     */
244
-    private ReactRootView reactRootView;
245
-
246 98
     /**
247 99
      * The URL of the current conference.
248 100
      */
@@ -258,34 +110,33 @@ public class JitsiMeetView extends FrameLayout {
258 110
     public JitsiMeetView(@NonNull Context context) {
259 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 113
         // The entry point into the invite feature of Jitsi Meet. The Java
273 114
         // counterpart of the JavaScript InviteButton.
274 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,7 +145,7 @@ public class JitsiMeetView extends FrameLayout {
294 145
      * partial URL (e.g. a room name only) is specified to
295 146
      * {@link #loadURLString(String)} or {@link #loadURLObject(Bundle)}. If not
296 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 150
      * @return The default base {@code URL} or {@code null}.
300 151
      */
@@ -337,7 +188,7 @@ public class JitsiMeetView extends FrameLayout {
337 188
         return
338 189
             PictureInPictureModule.isPictureInPictureSupported()
339 190
                 && (pictureInPictureEnabled == null
340
-                    || pictureInPictureEnabled.booleanValue());
191
+                    || pictureInPictureEnabled);
341 192
     }
342 193
 
343 194
     /**
@@ -395,9 +246,6 @@ public class JitsiMeetView extends FrameLayout {
395 246
             props.putString("defaultURL", defaultURL.toString());
396 247
         }
397 248
 
398
-        // externalAPIScope
399
-        props.putString("externalAPIScope", externalAPIScope);
400
-
401 249
         // inviteController
402 250
         InviteController inviteController = getInviteController();
403 251
 
@@ -434,17 +282,7 @@ public class JitsiMeetView extends FrameLayout {
434 282
         // per loadURLObject: invocation.
435 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,65 +306,50 @@ public class JitsiMeetView extends FrameLayout {
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 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,7 +386,7 @@ public class JitsiMeetView extends FrameLayout {
563 386
      * {@code true}; otherwise, {@code false}.
564 387
      */
565 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 Ver fichero

@@ -0,0 +1,150 @@
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 Ver fichero

@@ -0,0 +1,113 @@
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 Ver fichero

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

Loading…
Cancelar
Guardar