浏览代码

fix(Android|PiP): do not invoke 'enterPictureInPicture' in PAUSED state

Activity.enterPictureInPictureMode method must be invoked synchronously
on userLeaveHint callback in order to be sure that the current Activity
is still visible (does not transit to PAUSED state). Previously if the
asynchronous processing would be delayed enough for the Activity to go
into the PAUSED state it will be too late to go into the PiP mode.
master
paweldomas 7 年前
父节点
当前提交
565fd37f28

+ 41
- 1
android/sdk/src/main/java/org/jitsi/meet/sdk/ExternalAPIModule.java 查看文件

@@ -111,6 +111,34 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
111 111
         return "ExternalAPI";
112 112
     }
113 113
 
114
+    /**
115
+     * The internal processing for the conference URL set on
116
+     * a {@link JitsiMeetView} instance.
117
+     *
118
+     * @param eventName the name of the external API event to be processed.
119
+     * @param view the {@link JitsiMeetView} instance.
120
+     * @param url the "url" attribute value retrieved from the "data" carried by
121
+     * the event.
122
+     */
123
+    private void maybeSetConferenceUrlOnTheView(
124
+            String eventName, JitsiMeetView view, String url)
125
+    {
126
+        switch(eventName) {
127
+        case "CONFERENCE_WILL_JOIN":
128
+            view.setCurrentConferenceUrl(url);
129
+            break;
130
+
131
+        case "CONFERENCE_FAILED":
132
+        case "CONFERENCE_WILL_LEAVE":
133
+        case "LOAD_CONFIG_ERROR":
134
+            // Abandon the conference only if it's for the current URL
135
+            if (url != null && url.equals(view.getCurrentConferenceUrl())) {
136
+                view.setCurrentConferenceUrl(null);
137
+            }
138
+            break;
139
+        }
140
+    }
141
+
114 142
     /**
115 143
      * Dispatches an event that occurred on JavaScript to the view's listener.
116 144
      *
@@ -130,6 +158,8 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
130 158
             return;
131 159
         }
132 160
 
161
+        maybeSetConferenceUrlOnTheView(name, view, data.getString("url"));
162
+
133 163
         JitsiMeetViewListener listener = view.getListener();
134 164
 
135 165
         if (listener == null) {
@@ -141,7 +171,17 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
141 171
         if (method != null) {
142 172
             try {
143 173
                 method.invoke(listener, toHashMap(data));
144
-            } catch (IllegalAccessException | InvocationTargetException e) {
174
+            } catch (IllegalAccessException e) {
175
+                // FIXME There was a multicatch for IllegalAccessException and
176
+                // InvocationTargetException, but Android Studio complained
177
+                // with:
178
+                // "Multi-catch with these reflection exceptions requires
179
+                // API level 19 (current min is 16) because they get compiled to
180
+                // the common but new super type ReflectiveOperationException.
181
+                // As a workaround either create individual catch statements, or
182
+                // catch Exception."
183
+                throw new RuntimeException(e);
184
+            } catch (InvocationTargetException e) {
145 185
                 throw new RuntimeException(e);
146 186
             }
147 187
         }

+ 3
- 1
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java 查看文件

@@ -260,7 +260,9 @@ public class JitsiMeetActivity extends AppCompatActivity {
260 260
 
261 261
     @Override
262 262
     protected void onUserLeaveHint() {
263
-        JitsiMeetView.onUserLeaveHint();
263
+        if (view != null) {
264
+            view.onUserLeaveHint();
265
+        }
264 266
     }
265 267
 
266 268
     /**

+ 54
- 8
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java 查看文件

@@ -167,7 +167,7 @@ public class JitsiMeetView extends FrameLayout {
167 167
             DefaultHardwareBackBtnHandler defaultBackButtonImpl) {
168 168
         ReactInstanceManager reactInstanceManager
169 169
             = ReactInstanceManagerHolder.getReactInstanceManager();
170
-        
170
+
171 171
         if (reactInstanceManager != null) {
172 172
             reactInstanceManager.onHostResume(activity, defaultBackButtonImpl);
173 173
         }
@@ -205,15 +205,13 @@ public class JitsiMeetView extends FrameLayout {
205 205
     }
206 206
 
207 207
     /**
208
-     * Activity lifecycle method which should be called from
209
-     * {@code Activity.onUserLeaveHint} so we can do the required internal
210
-     * processing.
208
+     * Stores the current conference URL. Will have a value when the app is in
209
+     * a conference.
211 210
      *
212
-     * This is currently not mandatory.
211
+     * Currently one thread writes and one thread reads, so it should be fine to
212
+     * have this field volatile without additional synchronization.
213 213
      */
214
-    public static void onUserLeaveHint() {
215
-        ReactInstanceManagerHolder.emitEvent("onUserLeaveHint", null);
216
-    }
214
+    private volatile String conferenceUrl;
217 215
 
218 216
     /**
219 217
      * The default base {@code URL} used to join a conference when a partial URL
@@ -293,6 +291,16 @@ public class JitsiMeetView extends FrameLayout {
293 291
         }
294 292
     }
295 293
 
294
+    /**
295
+     * Retrieves the current conferences URL.
296
+     *
297
+     * @return a string with conference URL if the view is currently in
298
+     * a conference or {@code null} otherwise.
299
+     */
300
+    public String getCurrentConferenceUrl() {
301
+        return conferenceUrl;
302
+    }
303
+
296 304
     /**
297 305
      * Gets the default base {@code URL} used to join a conference when a
298 306
      * partial URL (e.g. a room name only) is specified to
@@ -458,6 +466,33 @@ public class JitsiMeetView extends FrameLayout {
458 466
         loadURLObject(urlObject);
459 467
     }
460 468
 
469
+    /**
470
+     * Activity lifecycle method which should be called from
471
+     * {@code Activity.onUserLeaveHint} so we can do the required internal
472
+     * processing.
473
+     *
474
+     * This is currently not mandatory, but if used will provide automatic
475
+     * handling of the picture in picture mode when user minimizes the app. It
476
+     * will be probably the most useful in case the app is using the welcome
477
+     * page.
478
+     */
479
+    public void onUserLeaveHint() {
480
+        if (getPictureInPictureEnabled() && conferenceUrl != null) {
481
+            PictureInPictureModule pipModule
482
+                = ReactInstanceManagerHolder.getNativeModule(
483
+                        PictureInPictureModule.class);
484
+
485
+            if (pipModule != null) {
486
+                try {
487
+                    pipModule.enterPictureInPicture();
488
+                } catch (RuntimeException exc) {
489
+                    Log.e(
490
+                        TAG, "onUserLeaveHint: failed to enter PiP mode", exc);
491
+                }
492
+            }
493
+        }
494
+    }
495
+
461 496
     /**
462 497
      * Called when the window containing this view gains or loses focus.
463 498
      *
@@ -495,6 +530,17 @@ public class JitsiMeetView extends FrameLayout {
495 530
         }
496 531
     }
497 532
 
533
+    /**
534
+     * Sets the current conference URL.
535
+     *
536
+     * @param conferenceUrl a string with new conference URL to set if the view
537
+     * is entering the conference or {@code null} if the view is no longer in
538
+     * the conference.
539
+     */
540
+    void setCurrentConferenceUrl(String conferenceUrl) {
541
+        this.conferenceUrl = conferenceUrl;
542
+    }
543
+
498 544
     /**
499 545
      * Sets the default base {@code URL} used to join a conference when a
500 546
      * partial URL (e.g. a room name only) is specified to

+ 43
- 36
android/sdk/src/main/java/org/jitsi/meet/sdk/PictureInPictureModule.java 查看文件

@@ -26,50 +26,57 @@ public class PictureInPictureModule extends ReactContextBaseJavaModule {
26 26
      * Enters Picture-in-Picture (mode) for the current {@link Activity}.
27 27
      * Supported on Android API >= 26 (Oreo) only.
28 28
      *
29
-     * @param promise a {@code Promise} which will resolve with a {@code null}
30
-     * value upon success, and an {@link Exception} otherwise.
29
+     * @throws IllegalStateException if {@link #isPictureInPictureSupported()}
30
+     * returns {@code false} or if {@link #getCurrentActivity()} returns
31
+     * {@code null}.
32
+     * @throws RuntimeException if
33
+     * {@link Activity#enterPictureInPictureMode(PictureInPictureParams)} fails.
34
+     * That method can also throw a {@link RuntimeException} in various cases,
35
+     * including when the activity is not visible (paused or stopped), if the
36
+     * screen is locked or if the user has an activity pinned.
31 37
      */
32
-    @ReactMethod
33
-    public void enterPictureInPicture(Promise promise) {
34
-        if (isPictureInPictureSupported()) {
35
-            Activity currentActivity = getCurrentActivity();
36
-
37
-            if (currentActivity == null) {
38
-                promise.reject(new Exception("No current Activity!"));
39
-                return;
40
-            }
38
+    public void enterPictureInPicture() {
39
+        if (!isPictureInPictureSupported()) {
40
+            throw new IllegalStateException("Picture-in-Picture not supported");
41
+        }
41 42
 
42
-            Log.d(TAG, "Entering Picture-in-Picture");
43
+        Activity currentActivity = getCurrentActivity();
43 44
 
44
-            PictureInPictureParams.Builder builder
45
-                = new PictureInPictureParams.Builder()
46
-                    .setAspectRatio(new Rational(1, 1));
47
-            Throwable error;
45
+        if (currentActivity == null) {
46
+            throw new IllegalStateException("No current Activity!");
47
+        }
48 48
 
49
-            // https://developer.android.com/reference/android/app/Activity.html#enterPictureInPictureMode(android.app.PictureInPictureParams)
50
-            //
51
-            // The system may disallow entering picture-in-picture in various
52
-            // cases, including when the activity is not visible, if the screen
53
-            // is locked or if the user has an activity pinned.
54
-            try {
55
-                error
56
-                    = currentActivity.enterPictureInPictureMode(builder.build())
57
-                        ? null
58
-                        : new Exception("Failed to enter Picture-in-Picture");
59
-            } catch (RuntimeException re) {
60
-                error = re;
61
-            }
49
+        Log.d(TAG, "Entering Picture-in-Picture");
62 50
 
63
-            if (error == null) {
64
-                promise.resolve(null);
65
-            } else {
66
-                promise.reject(error);
67
-            }
51
+        PictureInPictureParams.Builder builder
52
+            = new PictureInPictureParams.Builder()
53
+                .setAspectRatio(new Rational(1, 1));
68 54
 
69
-            return;
55
+        // https://developer.android.com/reference/android/app/Activity.html#enterPictureInPictureMode(android.app.PictureInPictureParams)
56
+        //
57
+        // The system may disallow entering picture-in-picture in various cases,
58
+        // including when the activity is not visible, if the screen is locked
59
+        // or if the user has an activity pinned.
60
+        if (!currentActivity.enterPictureInPictureMode(builder.build())) {
61
+            throw new RuntimeException("Failed to enter Picture-in-Picture");
70 62
         }
63
+    }
71 64
 
72
-        promise.reject(new Exception("Picture-in-Picture not supported"));
65
+    /**
66
+     * Enters Picture-in-Picture (mode) for the current {@link Activity}.
67
+     * Supported on Android API >= 26 (Oreo) only.
68
+     *
69
+     * @param promise a {@code Promise} which will resolve with a {@code null}
70
+     * value upon success, and an {@link Exception} otherwise.
71
+     */
72
+    @ReactMethod
73
+    public void enterPictureInPicture(Promise promise) {
74
+        try {
75
+            enterPictureInPicture();
76
+            promise.resolve(null);
77
+        } catch (RuntimeException re) {
78
+            promise.reject(re);
79
+        }
73 80
     }
74 81
 
75 82
     @Override

+ 21
- 1
android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java 查看文件

@@ -61,7 +61,7 @@ public class ReactInstanceManagerHolder {
61 61
             @Nullable Object data) {
62 62
         ReactInstanceManager reactInstanceManager
63 63
             = ReactInstanceManagerHolder.getReactInstanceManager();
64
-        
64
+
65 65
         if (reactInstanceManager != null) {
66 66
             ReactContext reactContext
67 67
                 = reactInstanceManager.getCurrentReactContext();
@@ -77,6 +77,26 @@ public class ReactInstanceManagerHolder {
77 77
         return false;
78 78
     }
79 79
 
80
+    /**
81
+     * Finds a native React module for given class.
82
+     *
83
+     * @param nativeModuleClass the native module's class for which an instance
84
+     * is to be retrieved from the {@link #reactInstanceManager}.
85
+     * @param <T> the module's type.
86
+     * @return {@link NativeModule} instance for given interface type or
87
+     * {@code null} if no instance for this interface is available, or if
88
+     * {@link #reactInstanceManager} has not been initialized yet.
89
+     */
90
+    static <T extends NativeModule> T getNativeModule(
91
+            Class<T> nativeModuleClass) {
92
+        ReactContext reactContext
93
+            = reactInstanceManager != null
94
+                ? reactInstanceManager.getCurrentReactContext() : null;
95
+
96
+        return reactContext != null
97
+                ? reactContext.getNativeModule(nativeModuleClass) : null;
98
+    }
99
+
80 100
     static ReactInstanceManager getReactInstanceManager() {
81 101
         return reactInstanceManager;
82 102
     }

+ 0
- 13
react/features/mobile/picture-in-picture/actionTypes.js 查看文件

@@ -9,16 +9,3 @@
9 9
  * @public
10 10
  */
11 11
 export const ENTER_PICTURE_IN_PICTURE = Symbol('ENTER_PICTURE_IN_PICTURE');
12
-
13
-/**
14
- * The type of redux action to set the {@code EventEmitter} subscriptions
15
- * utilized by the feature picture-in-picture.
16
- *
17
- * {
18
- *     type: _SET_EMITTER_SUBSCRIPTIONS,
19
- *     emitterSubscriptions: Array|undefined
20
- * }
21
- *
22
- * @protected
23
- */
24
-export const _SET_EMITTER_SUBSCRIPTIONS = Symbol('_SET_EMITTER_SUBSCRIPTIONS');

+ 1
- 23
react/features/mobile/picture-in-picture/actions.js 查看文件

@@ -4,10 +4,7 @@ import { NativeModules } from 'react-native';
4 4
 
5 5
 import { Platform } from '../../base/react';
6 6
 
7
-import {
8
-    ENTER_PICTURE_IN_PICTURE,
9
-    _SET_EMITTER_SUBSCRIPTIONS
10
-} from './actionTypes';
7
+import { ENTER_PICTURE_IN_PICTURE } from './actionTypes';
11 8
 
12 9
 /**
13 10
  * Enters (or rather initiates entering) picture-in-picture.
@@ -47,22 +44,3 @@ export function enterPictureInPicture() {
47 44
         }
48 45
     };
49 46
 }
50
-
51
-/**
52
- * Sets the {@code EventEmitter} subscriptions utilized by the feature
53
- * picture-in-picture.
54
- *
55
- * @param {Array<Object>} emitterSubscriptions - The {@code EventEmitter}
56
- * subscriptions to be set.
57
- * @protected
58
- * @returns {{
59
- *     type: _SET_EMITTER_SUBSCRIPTIONS,
60
- *     emitterSubscriptions: Array<Object>
61
- * }}
62
- */
63
-export function _setEmitterSubscriptions(emitterSubscriptions: ?Array<Object>) {
64
-    return {
65
-        type: _SET_EMITTER_SUBSCRIPTIONS,
66
-        emitterSubscriptions
67
-    };
68
-}

+ 0
- 3
react/features/mobile/picture-in-picture/index.js 查看文件

@@ -1,6 +1,3 @@
1 1
 export * from './actions';
2 2
 export * from './actionTypes';
3 3
 export * from './components';
4
-
5
-import './middleware';
6
-import './reducer';

+ 0
- 70
react/features/mobile/picture-in-picture/middleware.js 查看文件

@@ -1,70 +0,0 @@
1
-// @flow
2
-
3
-import { DeviceEventEmitter } from 'react-native';
4
-
5
-import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
6
-import { MiddlewareRegistry } from '../../base/redux';
7
-
8
-import { enterPictureInPicture, _setEmitterSubscriptions } from './actions';
9
-import { _SET_EMITTER_SUBSCRIPTIONS } from './actionTypes';
10
-
11
-/**
12
- * Middleware that handles Picture-in-Picture requests. Currently it enters
13
- * the native PiP mode on Android, when requested.
14
- *
15
- * @param {Store} store - Redux store.
16
- * @returns {Function}
17
- */
18
-MiddlewareRegistry.register(store => next => action => {
19
-    switch (action.type) {
20
-    case APP_WILL_MOUNT:
21
-        return _appWillMount(store, next, action);
22
-
23
-    case APP_WILL_UNMOUNT:
24
-        store.dispatch(_setEmitterSubscriptions(undefined));
25
-        break;
26
-
27
-    case _SET_EMITTER_SUBSCRIPTIONS: {
28
-        // Remove the current/old EventEmitter subscriptions.
29
-        const { emitterSubscriptions } = store.getState()['features/pip'];
30
-
31
-        if (emitterSubscriptions) {
32
-            for (const emitterSubscription of emitterSubscriptions) {
33
-                // XXX We may be removing an EventEmitter subscription which is
34
-                // in both the old and new Array of EventEmitter subscriptions!
35
-                // Thankfully, we don't have such a practical use case at the
36
-                // time of this writing.
37
-                emitterSubscription.remove();
38
-            }
39
-        }
40
-        break;
41
-    }
42
-    }
43
-
44
-    return next(action);
45
-});
46
-
47
-/**
48
- * Notifies the feature pip that the action {@link APP_WILL_MOUNT} is being
49
- * dispatched within a specific redux {@code store}.
50
- *
51
- * @param {Store} store - The redux store in which the specified {@code action}
52
- * is being dispatched.
53
- * @param {Dispatch} next - The redux dispatch function to dispatch the
54
- * specified {@code action} to the specified {@code store}.
55
- * @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is
56
- * being dispatched in the specified {@code store}.
57
- * @private
58
- * @returns {*} The value returned by {@code next(action)}.
59
- */
60
-function _appWillMount({ dispatch }, next, action) {
61
-    dispatch(_setEmitterSubscriptions([
62
-
63
-        // Android's onUserLeaveHint activity lifecycle callback
64
-        DeviceEventEmitter.addListener(
65
-            'onUserLeaveHint',
66
-            () => dispatch(enterPictureInPicture()))
67
-    ]));
68
-
69
-    return next(action);
70
-}

+ 0
- 17
react/features/mobile/picture-in-picture/reducer.js 查看文件

@@ -1,17 +0,0 @@
1
-// @flow
2
-
3
-import { ReducerRegistry } from '../../base/redux';
4
-
5
-import { _SET_EMITTER_SUBSCRIPTIONS } from './actionTypes';
6
-
7
-ReducerRegistry.register('features/pip', (state = {}, action) => {
8
-    switch (action.type) {
9
-    case _SET_EMITTER_SUBSCRIPTIONS:
10
-        return {
11
-            ...state,
12
-            emitterSubscriptions: action.emitterSubscriptions
13
-        };
14
-    }
15
-
16
-    return state;
17
-});

正在加载...
取消
保存