// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.customtabs.features;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
import static androidx.core.view.WindowInsetsCompat.Type.systemBars;
import static androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE;
import static androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
import android.app.Activity;
import android.os.Build;
import android.os.Handler;
import android.view.View;
import android.view.Window;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.display_cutout.ActivityDisplayCutoutModeSupplier;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.DestroyObserver;
import org.chromium.chrome.browser.lifecycle.WindowFocusChangedObserver;
import org.chromium.ui.base.WindowAndroid;
import javax.inject.Inject;
/** Allows to enter and exit immersive mode in TWAs and WebAPKs. */
@ActivityScope
public class ImmersiveModeController implements WindowFocusChangedObserver, DestroyObserver {
private static final int ENTER_IMMERSIVE_MODE_ON_WINDOW_FOCUS_DELAY_MILLIS = 300;
private static final int RESTORE_IMMERSIVE_MODE_DELAY_MILLIS = 3000;
private final Activity mActivity;
private final ActivityDisplayCutoutModeSupplier mCutoutSupplier =
new ActivityDisplayCutoutModeSupplier();
private final Handler mHandler = new Handler();
private final Runnable mUpdateImmersiveFlagsRunnable = this::updateImmersiveFlags;
private boolean mInImmersiveMode;
private boolean mIsImmersiveModeSticky;
private static final int IMMERSIVE_MODE_UI_FLAGS =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
| View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
| View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_IMMERSIVE;
private static final int IMMERSIVE_STICKY_MODE_UI_FLAGS =
(IMMERSIVE_MODE_UI_FLAGS & ~View.SYSTEM_UI_FLAG_IMMERSIVE)
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
@Inject
public ImmersiveModeController(
ActivityLifecycleDispatcher lifecycleDispatcher,
Activity activity,
WindowAndroid window) {
mActivity = activity;
lifecycleDispatcher.register(this);
mCutoutSupplier.attach(window.getUnownedUserDataHost());
}
/**
* Sets activity's decor view into an immersive mode and ensures it stays that way.
*
* @param layoutInDisplayCutoutMode Integer defining how to deal with cutouts, see
* {@link android.view.WindowManager.LayoutParams#layoutInDisplayCutoutMode} and
* https://developer.android.com/guide/topics/display-cutout
*
* @param sticky Whether {@link View#SYSTEM_UI_FLAG_IMMERSIVE} or
* {@link View#SYSTEM_UI_FLAG_IMMERSIVE_STICKY} should be used.
* See https://developer.android.com/training/system-ui/immersive#sticky-immersive
*/
public void enterImmersiveMode(int layoutInDisplayCutoutMode, boolean sticky) {
if (mInImmersiveMode) return;
mInImmersiveMode = true;
mIsImmersiveModeSticky = sticky;
Window window = mActivity.getWindow();
View decor = window.getDecorView();
// When we enter immersive mode for the first time, register a
// SystemUiVisibilityChangeListener that restores immersive mode. This is necessary
// because user actions like focusing a keyboard will break out of immersive mode.
decor.setOnSystemUiVisibilityChangeListener(
newFlags -> postSetImmersiveFlags(RESTORE_IMMERSIVE_MODE_DELAY_MILLIS));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// In order to avoid a flicker during launch, set the display cutout mode now (vs
// waiting for DisplayCutoutController to set the mode).
window.getAttributes().layoutInDisplayCutoutMode = layoutInDisplayCutoutMode;
mCutoutSupplier.set(layoutInDisplayCutoutMode);
}
postSetImmersiveFlags(0);
}
/** Exits immersive mode. */
public void exitImmersiveMode() {
if (!mInImmersiveMode) return;
mInImmersiveMode = false;
mHandler.removeCallbacks(mUpdateImmersiveFlagsRunnable);
updateImmersiveFlags();
mCutoutSupplier.set(LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT);
}
private void postSetImmersiveFlags(int delayInMills) {
if (!mInImmersiveMode) return;
mHandler.removeCallbacks(mUpdateImmersiveFlagsRunnable);
mHandler.postDelayed(mUpdateImmersiveFlagsRunnable, delayInMills);
}
private void updateImmersiveFlags() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
// For some reason, on Android R (11) setting the recommended immersive mode flags
// (including BEHAVIOR_SHOW_BARS_BY_SWIPE) gives the behaviour of
// BEHAVIOR_SHOW_BARS_BY_TOUCH. I can't reproduce this with a sample app, and I cannot
// reproduce it with Chrome on an emulator. https://crbug.com/1232956
updateImmersiveFlagsOnAndroid11();
} else {
updateImmersiveFlagsOnAndroidNot11();
}
Window window = mActivity.getWindow();
WindowCompat.setDecorFitsSystemWindows(window, !mInImmersiveMode);
}
private void updateImmersiveFlagsOnAndroid11() {
View decor = mActivity.getWindow().getDecorView();
int currentFlags = decor.getSystemUiVisibility();
int immersiveModeFlags =
mIsImmersiveModeSticky ? IMMERSIVE_STICKY_MODE_UI_FLAGS : IMMERSIVE_MODE_UI_FLAGS;
int desiredFlags =
mInImmersiveMode
? (currentFlags | immersiveModeFlags)
: (currentFlags & ~immersiveModeFlags);
if (currentFlags != desiredFlags) {
decor.setSystemUiVisibility(desiredFlags);
}
}
// BEHAVIOR_SHOW_BARS_BY_SWIPE is deprecated.
@SuppressWarnings("WrongConstant")
private void updateImmersiveFlagsOnAndroidNot11() {
Window window = mActivity.getWindow();
View decor = window.getDecorView();
WindowInsetsControllerCompat insetsController =
WindowCompat.getInsetsController(window, decor);
assert insetsController != null : "Decor View isn't attached to the Window.";
if (mIsImmersiveModeSticky) {
insetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
} else {
insetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_BARS_BY_SWIPE);
}
if (mInImmersiveMode) {
insetsController.hide(systemBars());
} else {
insetsController.show(systemBars());
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus && mInImmersiveMode) {
postSetImmersiveFlags(ENTER_IMMERSIVE_MODE_ON_WINDOW_FOCUS_DELAY_MILLIS);
}
}
@Override
public void onDestroy() {
mHandler.removeCallbacks(mUpdateImmersiveFlagsRunnable);
mCutoutSupplier.destroy();
}
}