From 4757c1ebca8321a346008ddac2bc737446c16c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Wed, 7 Feb 2018 14:34:40 +0100 Subject: [PATCH] [RN] Make full-screen more resilient on Android On Android we go into "immersive mode" when in a conference, this is our way of being full-creen. There are occasions, however, in which Android takes us out of immerfive mode without us (the application / SDK) knowing: when a child activity is started, a modal window shown, etc. In order to be resilient to any possible change in the immersive mode, register a listener which will be called when Android changes it, so we can re-eavluate if we need it and thus re-enable it. --- .../org/jitsi/meet/sdk/JitsiMeetView.java | 19 ++++ react/features/mobile/background/reducer.js | 36 ++++---- .../mobile/full-screen/actionTypes.js | 11 +++ react/features/mobile/full-screen/actions.js | 20 +++++ react/features/mobile/full-screen/index.js | 1 + .../features/mobile/full-screen/middleware.js | 87 +++++++++++++------ react/features/mobile/full-screen/reducer.js | 21 +++++ 7 files changed, 155 insertions(+), 40 deletions(-) create mode 100644 react/features/mobile/full-screen/actionTypes.js create mode 100644 react/features/mobile/full-screen/actions.js create mode 100644 react/features/mobile/full-screen/reducer.js diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java index 6a16b1c9f..0238390b3 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java @@ -32,6 +32,7 @@ import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.common.LifecycleState; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.rnimmersive.RNImmersiveModule; import java.net.URL; import java.util.Arrays; @@ -414,6 +415,24 @@ public class JitsiMeetView extends FrameLayout { loadURLObject(urlObject); } + /** + * Handler for focus changes which the window where this view is attached to + * is experiencing. Here we call into the Immersive mode plugin, so it + * triggers an event. + * + * @param hasFocus - Whether the window / view has focus or not. + */ + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + RNImmersiveModule module = RNImmersiveModule.getInstance(); + + if (hasFocus && module != null) { + module.emitImmersiveStateChangeEvent(); + } + } + /** * Sets the default base {@code URL} used to join a conference when a * partial URL (e.g. a room name only) is specified to diff --git a/react/features/mobile/background/reducer.js b/react/features/mobile/background/reducer.js index 21c618ac7..041abdba3 100644 --- a/react/features/mobile/background/reducer.js +++ b/react/features/mobile/background/reducer.js @@ -5,20 +5,26 @@ import { APP_STATE_CHANGED } from './actionTypes'; -ReducerRegistry.register('features/background', (state = {}, action) => { - switch (action.type) { - case _SET_APP_STATE_LISTENER: - return { - ...state, - appStateListener: action.listener - }; +const INITIAL_STATE = { + appState: 'active' +}; - case APP_STATE_CHANGED: - return { - ...state, - appState: action.appState - }; - } +ReducerRegistry.register( + 'features/background', + (state = INITIAL_STATE, action) => { + switch (action.type) { + case _SET_APP_STATE_LISTENER: + return { + ...state, + appStateListener: action.listener + }; - return state; -}); + case APP_STATE_CHANGED: + return { + ...state, + appState: action.appState + }; + } + + return state; + }); diff --git a/react/features/mobile/full-screen/actionTypes.js b/react/features/mobile/full-screen/actionTypes.js new file mode 100644 index 000000000..e9aadbee4 --- /dev/null +++ b/react/features/mobile/full-screen/actionTypes.js @@ -0,0 +1,11 @@ +/** + * The type of redux action to set the Immersive change event listener. + * + * { + * type: _SET_IMMERSIVE_LISTENER, + * listener: Function + * } + * + * @protected + */ +export const _SET_IMMERSIVE_LISTENER = Symbol('_SET_IMMERSIVE_LISTENER'); diff --git a/react/features/mobile/full-screen/actions.js b/react/features/mobile/full-screen/actions.js new file mode 100644 index 000000000..52b7bd7b7 --- /dev/null +++ b/react/features/mobile/full-screen/actions.js @@ -0,0 +1,20 @@ +// @flow + +import { _SET_IMMERSIVE_LISTENER } from './actionTypes'; + +/** + * Sets the listener to be used with React Native's Immersive API. + * + * @param {Function} listener - Function to be set as the change event listener. + * @protected + * @returns {{ + * type: _SET_IMMERSIVE_LISTENER, + * listener: Function + * }} + */ +export function _setImmersiveListener(listener: ?Function) { + return { + type: _SET_IMMERSIVE_LISTENER, + listener + }; +} diff --git a/react/features/mobile/full-screen/index.js b/react/features/mobile/full-screen/index.js index d43689289..200d25492 100644 --- a/react/features/mobile/full-screen/index.js +++ b/react/features/mobile/full-screen/index.js @@ -1 +1,2 @@ import './middleware'; +import './reducer'; diff --git a/react/features/mobile/full-screen/middleware.js b/react/features/mobile/full-screen/middleware.js index 4ff76db44..3974a517c 100644 --- a/react/features/mobile/full-screen/middleware.js +++ b/react/features/mobile/full-screen/middleware.js @@ -1,20 +1,22 @@ -/* @flow */ +// @flow import { StatusBar } from 'react-native'; import { Immersive } from 'react-native-immersive'; -import { APP_STATE_CHANGED } from '../background'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app'; import { CONFERENCE_FAILED, + CONFERENCE_JOINED, CONFERENCE_LEFT, CONFERENCE_WILL_JOIN, SET_AUDIO_ONLY } from '../../base/conference'; -import { HIDE_DIALOG } from '../../base/dialog'; import { Platform } from '../../base/react'; -import { SET_REDUCED_UI } from '../../base/responsive-ui'; import { MiddlewareRegistry } from '../../base/redux'; +import { _setImmersiveListener } from './actions'; +import { _SET_IMMERSIVE_LISTENER } from './actionTypes'; + /** * Middleware that captures conference actions and activates or deactivates the * full screen mode. On iOS it hides the status bar, and on Android it uses the @@ -26,26 +28,43 @@ import { MiddlewareRegistry } from '../../base/redux'; * @param {Store} store - The redux store. * @returns {Function} */ -MiddlewareRegistry.register(({ getState }) => next => action => { +MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { const result = next(action); let fullScreen = null; switch (action.type) { - case APP_STATE_CHANGED: - case CONFERENCE_WILL_JOIN: - case HIDE_DIALOG: - case SET_AUDIO_ONLY: - case SET_REDUCED_UI: { - // FIXME: Simplify this by listening to Immediate events. - // Check if we just came back from the background and re-enable full - // screen mode if necessary. - const { appState } = action; + case _SET_IMMERSIVE_LISTENER: + // XXX The React Native module Immersive is only implemented on Android + // and throws on other platforms. + if (Platform.OS === 'android') { + // Remove the current/old Immersive listener. + const { listener } = getState()['features/full-screen']; - if (typeof appState !== 'undefined' && appState !== 'active') { - break; + listener && Immersive.removeImmersiveListener(listener); + + // Add the new listener. + action.listener && Immersive.addImmersiveListener(action.listener); } + break; + case APP_WILL_MOUNT: { + const context = { + dispatch, + getState + }; + + dispatch( + _setImmersiveListener(_onImmersiveChange.bind(undefined, context))); + break; + } + case APP_WILL_UNMOUNT: + _setImmersiveListener(undefined); + break; + + case CONFERENCE_WILL_JOIN: + case CONFERENCE_JOINED: + case SET_AUDIO_ONLY: { const { audioOnly, conference, joining } = getState()['features/base/conference']; @@ -59,15 +78,33 @@ MiddlewareRegistry.register(({ getState }) => next => action => { break; } - if (fullScreen !== null) { - _setFullScreen(fullScreen) - .catch(err => - console.warn(`Failed to set full screen mode: ${err}`)); - } + fullScreen !== null && _setFullScreen(fullScreen); return result; }); +/** + * Handler for Immersive mode changes. This will be called when Android's + * immersive mode changes. This can happen without us wanting, so re-evaluate if + * immersive mode is desired and reactivate it if needed. + * + * @param {Object} store - The redux store. + * @private + * @returns {void} + */ +function _onImmersiveChange({ getState }) { + const state = getState(); + const { appState } = state['features/background']; + + if (appState === 'active') { + const { audioOnly, conference, joining } + = state['features/base/conference']; + const fullScreen = conference || joining ? !audioOnly : false; + + _setFullScreen(fullScreen); + } +} + /** * Activates/deactivates the full screen mode. On iOS it will hide the status * bar, and on Android it will turn immersive mode on. @@ -75,18 +112,18 @@ MiddlewareRegistry.register(({ getState }) => next => action => { * @param {boolean} fullScreen - True to set full screen mode, false to * deactivate it. * @private - * @returns {Promise} + * @returns {void} */ function _setFullScreen(fullScreen: boolean) { // XXX The React Native module Immersive is only implemented on Android and // throws on other platforms. if (Platform.OS === 'android') { - return fullScreen ? Immersive.on() : Immersive.off(); + fullScreen ? Immersive.on() : Immersive.off(); + + return; } // On platforms other than Android go with whatever React Native itself // supports. StatusBar.setHidden(fullScreen, 'slide'); - - return Promise.resolve(); } diff --git a/react/features/mobile/full-screen/reducer.js b/react/features/mobile/full-screen/reducer.js new file mode 100644 index 000000000..1e04f9692 --- /dev/null +++ b/react/features/mobile/full-screen/reducer.js @@ -0,0 +1,21 @@ +import { ReducerRegistry } from '../../base/redux'; + +import { _SET_IMMERSIVE_LISTENER } from './actionTypes'; + +const INITIAL_STATE = { + listener: undefined +}; + +ReducerRegistry.register( + 'features/full-screen', + (state = INITIAL_STATE, action) => { + switch (action.type) { + case _SET_IMMERSIVE_LISTENER: + return { + ...state, + listener: action.listener + }; + } + + return state; + });