| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // 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.fullscreen; |
| |
| import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; |
| import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; |
| import static android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE; |
| |
| import android.app.Activity; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.View.OnLayoutChangeListener; |
| import android.view.Window; |
| import android.view.WindowManager; |
| |
| import androidx.annotation.Nullable; |
| import androidx.core.util.ObjectsCompat; |
| |
| import org.chromium.base.supplier.ObservableSupplier; |
| import org.chromium.base.supplier.ObservableSupplierImpl; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.tab.TabUtils; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.ui.widget.Toast; |
| |
| import java.lang.ref.WeakReference; |
| |
| /** |
| * Handles updating the UI based on requests to the HTML Fullscreen API. |
| */ |
| public class FullscreenHtmlApiHandler { |
| private static final int MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS = 1; |
| private static final int MSG_ID_CLEAR_LAYOUT_FULLSCREEN_FLAG = 2; |
| |
| // The time we allow the Android notification bar to be shown when it is requested temporarily |
| // by the Android system (this value is additive on top of the show duration imposed by |
| // Android). |
| private static final long ANDROID_CONTROLS_SHOW_DURATION_MS = 200; |
| // Delay to allow a frame to render between getting the fullscreen layout update and clearing |
| // the SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN flag. |
| private static final long CLEAR_LAYOUT_FULLSCREEN_DELAY_MS = 20; |
| |
| private final Window mWindow; |
| private final Handler mHandler; |
| private final FullscreenHtmlApiDelegate mDelegate; |
| private final ObservableSupplierImpl<Boolean> mPersistentModeSupplier; |
| |
| // We need to cache WebContents/ContentView since we are setting fullscreen UI state on |
| // the WebContents's container view, and a Tab can change to have null web contents/ |
| // content view, i.e., if you navigate to a native page. |
| @Nullable |
| private WebContents mWebContentsInFullscreen; |
| @Nullable |
| private View mContentViewInFullscreen; |
| @Nullable private Tab mTabInFullscreen; |
| private FullscreenOptions mFullscreenOptions; |
| |
| // Toast at the top of the screen that is shown when user enters fullscreen for the |
| // first time. |
| private Toast mNotificationToast; |
| |
| private OnLayoutChangeListener mFullscreenOnLayoutChangeListener; |
| |
| /** |
| * Delegate that allows embedders to react to fullscreen API requests. |
| */ |
| public interface FullscreenHtmlApiDelegate { |
| /** |
| * Notifies the delegate that entering fullscreen has been requested and allows them |
| * to hide their controls. |
| * <p> |
| * Once the delegate has hidden the their controls, it must call |
| * {@link FullscreenHtmlApiHandler#enterFullscreen(Tab)}. |
| */ |
| void onEnterFullscreen(FullscreenOptions options); |
| |
| /** |
| * Cancels a pending enter fullscreen request if present. |
| * @return Whether the request was cancelled. |
| */ |
| boolean cancelPendingEnterFullscreen(); |
| |
| /** |
| * Notifies the delegate that the window UI has fully exited fullscreen and gives |
| * the embedder a chance to update their controls. |
| * |
| * @param tab The tab whose fullscreen is being exited. |
| */ |
| void onFullscreenExited(Tab tab); |
| |
| /** |
| * @return Whether the notification toast should be shown. For fullscreen video in |
| * overlay mode, the notification toast should be disabled. |
| */ |
| boolean shouldShowNotificationToast(); |
| } |
| |
| // This static inner class holds a WeakReference to the outer object, to avoid triggering the |
| // lint HandlerLeak warning. |
| private static class FullscreenHandler extends Handler { |
| private final WeakReference<FullscreenHtmlApiHandler> mFullscreenHtmlApiHandler; |
| |
| public FullscreenHandler(FullscreenHtmlApiHandler fullscreenHtmlApiHandler) { |
| mFullscreenHtmlApiHandler = new WeakReference<FullscreenHtmlApiHandler>( |
| fullscreenHtmlApiHandler); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| if (msg == null) return; |
| FullscreenHtmlApiHandler fullscreenHtmlApiHandler = mFullscreenHtmlApiHandler.get(); |
| if (fullscreenHtmlApiHandler == null) return; |
| |
| final WebContents webContents = fullscreenHtmlApiHandler.mWebContentsInFullscreen; |
| if (webContents == null) return; |
| |
| final View contentView = fullscreenHtmlApiHandler.mContentViewInFullscreen; |
| if (contentView == null) return; |
| int systemUiVisibility = contentView.getSystemUiVisibility(); |
| |
| switch (msg.what) { |
| case MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS: { |
| assert fullscreenHtmlApiHandler.getPersistentFullscreenMode() : |
| "Calling after we exited fullscreen"; |
| |
| if ((systemUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) |
| == SYSTEM_UI_FLAG_FULLSCREEN) { |
| return; |
| } |
| systemUiVisibility = fullscreenHtmlApiHandler.applyEnterFullscreenUIFlags( |
| systemUiVisibility); |
| contentView.setSystemUiVisibility(systemUiVisibility); |
| |
| // Trigger a update to clear the SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN flag |
| // once the view has been laid out after this system UI update. Without |
| // clearing this flag, the keyboard appearing will not trigger a relayout |
| // of the contents, which prevents updating the overdraw amount to the |
| // renderer. |
| contentView.addOnLayoutChangeListener(new OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange(View v, int left, int top, int right, |
| int bottom, int oldLeft, int oldTop, int oldRight, |
| int oldBottom) { |
| sendEmptyMessageDelayed(MSG_ID_CLEAR_LAYOUT_FULLSCREEN_FLAG, |
| CLEAR_LAYOUT_FULLSCREEN_DELAY_MS); |
| contentView.removeOnLayoutChangeListener(this); |
| } |
| }); |
| |
| contentView.requestLayout(); |
| break; |
| } |
| case MSG_ID_CLEAR_LAYOUT_FULLSCREEN_FLAG: { |
| // Change this assert to simply ignoring the message to work around |
| // https://crbug/365638 |
| // TODO(aberent): Fix bug |
| // assert getPersistentFullscreenMode() : "Calling after we exited fullscreen"; |
| if (!fullscreenHtmlApiHandler.getPersistentFullscreenMode()) return; |
| |
| if ((systemUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0) { |
| return; |
| } |
| systemUiVisibility &= ~SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; |
| contentView.setSystemUiVisibility(systemUiVisibility); |
| fullscreenHtmlApiHandler.clearWindowFlags( |
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); |
| break; |
| } |
| default: |
| assert false : "Unexpected message for ID: " + msg.what; |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Constructs the handler that will manage the UI transitions from the HTML fullscreen API. |
| * |
| * @param window The window containing the view going to fullscreen. |
| * @param delegate The delegate that allows embedders to handle fullscreen transitions. |
| */ |
| public FullscreenHtmlApiHandler(Window window, FullscreenHtmlApiDelegate delegate) { |
| mWindow = window; |
| mDelegate = delegate; |
| mHandler = new FullscreenHandler(this); |
| |
| mPersistentModeSupplier = new ObservableSupplierImpl<>(); |
| mPersistentModeSupplier.set(false); |
| } |
| |
| /** |
| * Enters persistent fullscreen mode. In this mode, the browser controls will be |
| * permanently hidden until this mode is exited. |
| * |
| * @param options Options to choose mode of fullscreen. |
| */ |
| public void enterPersistentFullscreenMode(FullscreenOptions options) { |
| if (getPersistentFullscreenMode() && ObjectsCompat.equals(mFullscreenOptions, options)) { |
| return; |
| } |
| |
| mPersistentModeSupplier.set(true); |
| mDelegate.onEnterFullscreen(options); |
| } |
| |
| /** |
| * Exits persistent fullscreen mode. Will restore browser controls visibility |
| * if they have been hidden. |
| */ |
| public void exitPersistentFullscreenMode() { |
| if (!getPersistentFullscreenMode()) return; |
| |
| mPersistentModeSupplier.set(false); |
| |
| if (mWebContentsInFullscreen != null && mTabInFullscreen != null) { |
| exitFullscreen(mWebContentsInFullscreen, mContentViewInFullscreen, mTabInFullscreen); |
| } else { |
| if (!mDelegate.cancelPendingEnterFullscreen()) { |
| assert false : "No content view previously set to fullscreen."; |
| } |
| } |
| mWebContentsInFullscreen = null; |
| mContentViewInFullscreen = null; |
| mTabInFullscreen = null; |
| mFullscreenOptions = null; |
| } |
| |
| /** |
| * @return Whether the application is in persistent fullscreen mode. |
| * @see #setPersistentFullscreenMode(boolean) |
| */ |
| public boolean getPersistentFullscreenMode() { |
| return mPersistentModeSupplier.get(); |
| } |
| |
| /** |
| * @return An observable supplier that determines whether the app is in persistent fullscreen |
| * mode. |
| */ |
| public ObservableSupplier<Boolean> getPersistentFullscreenModeSupplier() { |
| return mPersistentModeSupplier; |
| } |
| |
| private void exitFullscreen(WebContents webContents, View contentView, Tab tab) { |
| hideNotificationToast(); |
| mHandler.removeMessages(MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS); |
| mHandler.removeMessages(MSG_ID_CLEAR_LAYOUT_FULLSCREEN_FLAG); |
| |
| int systemUiVisibility = contentView.getSystemUiVisibility(); |
| systemUiVisibility &= ~SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; |
| systemUiVisibility = applyExitFullscreenUIFlags(systemUiVisibility); |
| clearWindowFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); |
| contentView.setSystemUiVisibility(systemUiVisibility); |
| if (mFullscreenOnLayoutChangeListener != null) { |
| contentView.removeOnLayoutChangeListener(mFullscreenOnLayoutChangeListener); |
| } |
| mFullscreenOnLayoutChangeListener = new OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange(View v, int left, int top, int right, int bottom, |
| int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| if ((bottom - top) < (oldBottom - oldTop)) { |
| mDelegate.onFullscreenExited(tab); |
| contentView.removeOnLayoutChangeListener(this); |
| } |
| } |
| }; |
| contentView.addOnLayoutChangeListener(mFullscreenOnLayoutChangeListener); |
| |
| if (webContents != null && !webContents.isDestroyed()) webContents.exitFullscreen(); |
| } |
| |
| /** |
| * Handles hiding the system UI components to allow the content to take up the full screen. |
| * @param tab The tab that is entering fullscreen. |
| */ |
| public void enterFullscreen(final Tab tab, FullscreenOptions options) { |
| WebContents webContents = tab.getWebContents(); |
| if (webContents == null) return; |
| mFullscreenOptions = options; |
| final View contentView = tab.getContentView(); |
| int systemUiVisibility = contentView.getSystemUiVisibility(); |
| if ((systemUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) == SYSTEM_UI_FLAG_FULLSCREEN) { |
| // Already in full screen mode; just changed options. Mask off old |
| // ones and apply new ones. |
| systemUiVisibility = applyExitFullscreenUIFlags(systemUiVisibility); |
| systemUiVisibility = applyEnterFullscreenUIFlags(systemUiVisibility); |
| } else if ((systemUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) |
| == SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) { |
| systemUiVisibility = applyEnterFullscreenUIFlags(systemUiVisibility); |
| } else { |
| Activity activity = TabUtils.getActivity(tab); |
| boolean isMultiWindow = MultiWindowUtils.getInstance().isLegacyMultiWindow(activity) |
| || MultiWindowUtils.getInstance().isInMultiWindowMode(activity); |
| |
| // To avoid a double layout that is caused by the system when just hiding |
| // the status bar set the status bar as translucent immediately. This cause |
| // it not to take up space so the layout is stable. (See crbug.com/935015). Do |
| // not do this in multi-window mode since that mode forces the status bar |
| // to always be visible. |
| if (mFullscreenOptions != null && mFullscreenOptions.showNavigationBar() |
| && !isMultiWindow) { |
| setWindowFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); |
| } |
| systemUiVisibility |= SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; |
| } |
| if (mFullscreenOnLayoutChangeListener != null) { |
| contentView.removeOnLayoutChangeListener(mFullscreenOnLayoutChangeListener); |
| } |
| mFullscreenOnLayoutChangeListener = new OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange(View v, int left, int top, int right, int bottom, |
| int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| // On certain sites playing embedded video (http://crbug.com/293782), setting the |
| // SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN does not always trigger a view-level layout |
| // with an updated height. To work around this, do not check for an increased |
| // height and always just trigger the next step of the fullscreen initialization. |
| // Posting the message to set the fullscreen flag because setting it directly in the |
| // onLayoutChange would have no effect. |
| mHandler.sendEmptyMessage(MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS); |
| |
| if ((bottom - top) <= (oldBottom - oldTop)) return; |
| if (mDelegate.shouldShowNotificationToast()) { |
| showNotificationToast(); |
| } |
| contentView.removeOnLayoutChangeListener(this); |
| } |
| }; |
| contentView.addOnLayoutChangeListener(mFullscreenOnLayoutChangeListener); |
| contentView.setSystemUiVisibility(systemUiVisibility); |
| mFullscreenOptions = options; |
| |
| // Request a layout so the updated system visibility takes affect. |
| contentView.requestLayout(); |
| |
| mWebContentsInFullscreen = webContents; |
| mContentViewInFullscreen = contentView; |
| mTabInFullscreen = tab; |
| } |
| |
| /** |
| * Create and show the fullscreen notification toast. |
| */ |
| private void showNotificationToast() { |
| if (mNotificationToast == null) { |
| int resId = R.string.immersive_fullscreen_api_notification; |
| mNotificationToast = Toast.makeText( |
| mWindow.getContext(), resId, Toast.LENGTH_LONG); |
| mNotificationToast.setGravity(Gravity.TOP | Gravity.CENTER, 0, 0); |
| } |
| mNotificationToast.show(); |
| } |
| |
| /** |
| * Hides the notification toast. |
| */ |
| public void hideNotificationToast() { |
| if (mNotificationToast != null) { |
| mNotificationToast.cancel(); |
| } |
| } |
| |
| /** |
| * Notified when the system UI visibility for the current ContentView has changed. |
| * @param visibility The updated UI visibility. |
| * @see View#getSystemUiVisibility() |
| */ |
| public void onContentViewSystemUiVisibilityChange(int visibility) { |
| if (mTabInFullscreen == null || !getPersistentFullscreenMode()) return; |
| mHandler.sendEmptyMessageDelayed( |
| MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS, ANDROID_CONTROLS_SHOW_DURATION_MS); |
| } |
| |
| /** |
| * Ensure the proper system UI flags are set after the window regains focus. |
| * @see android.app.Activity#onWindowFocusChanged(boolean) |
| */ |
| public void onWindowFocusChanged(boolean hasWindowFocus) { |
| if (!hasWindowFocus) hideNotificationToast(); |
| |
| mHandler.removeMessages(MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS); |
| mHandler.removeMessages(MSG_ID_CLEAR_LAYOUT_FULLSCREEN_FLAG); |
| if (mTabInFullscreen == null || !getPersistentFullscreenMode() || !hasWindowFocus) return; |
| mHandler.sendEmptyMessageDelayed( |
| MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS, ANDROID_CONTROLS_SHOW_DURATION_MS); |
| } |
| |
| /* |
| * Returns system ui flags to enable fullscreen mode based on the current options. |
| * @return fullscreen flags to be applied to system UI visibility. |
| */ |
| private int applyEnterFullscreenUIFlags(int systemUiVisibility) { |
| boolean showNavigationBar = |
| mFullscreenOptions != null ? mFullscreenOptions.showNavigationBar() : false; |
| int flags = SYSTEM_UI_FLAG_FULLSCREEN; |
| flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; |
| if (!showNavigationBar) { |
| flags |= SYSTEM_UI_FLAG_LOW_PROFILE; |
| flags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; |
| flags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; |
| } |
| return flags | systemUiVisibility; |
| } |
| |
| /* |
| * Returns system ui flags with any flags that might have been set during |
| * applyEnterFullscreenUIFlags masked off. |
| * @return fullscreen flags to be applied to system UI visibility. |
| */ |
| private static int applyExitFullscreenUIFlags(int systemUiVisibility) { |
| int maskOffFlags = SYSTEM_UI_FLAG_LOW_PROFILE | SYSTEM_UI_FLAG_FULLSCREEN; |
| maskOffFlags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; |
| maskOffFlags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; |
| maskOffFlags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; |
| return systemUiVisibility & ~maskOffFlags; |
| } |
| |
| /* |
| * Clears the current window attributes to not contain windowFlags. This |
| * is slightly different that mWindow.clearFlags which then sets a |
| * forced window attribute on the Window object that cannot be cleared. |
| */ |
| private void clearWindowFlags(int windowFlags) { |
| final WindowManager.LayoutParams attrs = mWindow.getAttributes(); |
| if ((attrs.flags & windowFlags) != 0) { |
| attrs.flags &= ~windowFlags; |
| mWindow.setAttributes(attrs); |
| } |
| } |
| |
| /* |
| * Sets the current window attributes to contain windowFlags. This |
| * is slightly different that mWindow.setFlags which then sets a |
| * forced window attribute on the Window object that cannot be cleared. |
| */ |
| private void setWindowFlags(int windowFlags) { |
| final WindowManager.LayoutParams attrs = mWindow.getAttributes(); |
| attrs.flags |= windowFlags; |
| mWindow.setAttributes(attrs); |
| } |
| } |