[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.
This commit is contained in:
Saúl Ibarra Corretgé 2018-02-07 14:34:40 +01:00 committed by Lyubo Marinov
parent 59d046dca9
commit 4757c1ebca
7 changed files with 155 additions and 40 deletions

View File

@ -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

View File

@ -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;
});

View File

@ -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');

View File

@ -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
};
}

View File

@ -1 +1,2 @@
import './middleware';
import './reducer';

View File

@ -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();
}

View File

@ -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;
});