// 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.os.Build; 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 org.chromium.chrome.R; import org.chromium.chrome.browser.tab.Tab; import org.chromium.content.browser.ContentViewCore; 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; // We still need this since we are setting fullscreen UI state on the contentviewcore's // container view, and a tab can have null content view core, i.e., if you navigate // to a native page. private ContentViewCore mContentViewCoreInFullscreen; private Tab mTabInFullscreen; private boolean mIsPersistentMode; // 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(); /** * 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; switch (msg.what) { case MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS: { assert fullscreenHtmlApiHandler.getPersistentFullscreenMode() : "Calling after we exited fullscreen"; final ContentViewCore contentViewCore = fullscreenHtmlApiHandler.mContentViewCoreInFullscreen; if (contentViewCore == null) return; final View contentView = contentViewCore.getContainerView(); int systemUiVisibility = contentView.getSystemUiVisibility(); if ((systemUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) == SYSTEM_UI_FLAG_FULLSCREEN) { return; } systemUiVisibility |= SYSTEM_UI_FLAG_FULLSCREEN; systemUiVisibility |= SYSTEM_UI_FLAG_LOW_PROFILE; systemUiVisibility |= getExtraFullscreenUIFlags(); 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); } }); break; } case MSG_ID_CLEAR_LAYOUT_FULLSCREEN_FLAG: { // Change this assert to simply ignoring the message to work around // http://crbug/365638 // TODO(aberent): Fix bug // assert mIsPersistentMode : "Calling after we exited fullscreen"; if (!fullscreenHtmlApiHandler.getPersistentFullscreenMode()) return; final ContentViewCore contentViewCore = fullscreenHtmlApiHandler.mContentViewCoreInFullscreen; if (contentViewCore == null) return; final View view = contentViewCore.getContainerView(); int systemUiVisibility = view.getSystemUiVisibility(); if ((systemUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0) { return; } systemUiVisibility &= ~SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; view.setSystemUiVisibility(systemUiVisibility); 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); } /** * Enters or exits persistent fullscreen mode. In this mode, the top controls will be * permanently hidden until this mode is exited. * * @param enabled Whether to enable persistent fullscreen mode. */ public void setPersistentFullscreenMode(boolean enabled) { if (mIsPersistentMode == enabled) return; mIsPersistentMode = enabled; if (mIsPersistentMode) { mDelegate.onEnterFullscreen(); } else { if (mContentViewCoreInFullscreen != null && mTabInFullscreen != null) { exitFullscreen(mContentViewCoreInFullscreen, mTabInFullscreen); } else { if (!mDelegate.cancelPendingEnterFullscreen()) { assert false : "No content view previously set to fullscreen."; } } mContentViewCoreInFullscreen = null; mTabInFullscreen = null; } } /** * @return Whether the application is in persistent fullscreen mode. * @see #setPersistentFullscreenMode(boolean) */ public boolean getPersistentFullscreenMode() { return mIsPersistentMode; } private void exitFullscreen(final ContentViewCore contentViewCore, final Tab tab) { final View contentView = contentViewCore.getContainerView(); 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_LOW_PROFILE; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { systemUiVisibility &= ~SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; systemUiVisibility &= ~SYSTEM_UI_FLAG_FULLSCREEN; systemUiVisibility &= ~getExtraFullscreenUIFlags(); } else { mWindow.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); mWindow.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } 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); // getWebContents() will return null if contentViewCore has been destroyed if (contentViewCore.getWebContents() != null) { contentViewCore.getWebContents().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) { ContentViewCore contentViewCore = tab.getContentViewCore(); if (contentViewCore == null) return; final View contentView = contentViewCore.getContainerView(); int systemUiVisibility = contentView.getSystemUiVisibility(); systemUiVisibility |= SYSTEM_UI_FLAG_LOW_PROFILE; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { if ((systemUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) { systemUiVisibility |= SYSTEM_UI_FLAG_FULLSCREEN; systemUiVisibility |= getExtraFullscreenUIFlags(); } else { systemUiVisibility |= SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; } } else { mWindow.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); mWindow.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_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. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { // 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); mContentViewCoreInFullscreen = contentViewCore; mTabInFullscreen = tab; } /** * Create and show the fullscreen notification toast. */ private void showNotificationToast() { if (mNotificationToast == null) { int resId = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) ? R.string.immersive_fullscreen_api_notification : R.string.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 (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return; if (mTabInFullscreen == null || !mIsPersistentMode) 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(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return; mHandler.removeMessages(MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS); mHandler.removeMessages(MSG_ID_CLEAR_LAYOUT_FULLSCREEN_FLAG); if (mTabInFullscreen == null || !mIsPersistentMode || !hasWindowFocus) return; mHandler.sendEmptyMessageDelayed( MSG_ID_SET_FULLSCREEN_SYSTEM_UI_FLAGS, ANDROID_CONTROLS_SHOW_DURATION_MS); } /* * Helper method to return extra fullscreen UI flags for Kitkat devices. * @return fullscreen flags to be applied to system UI visibility. */ private static int getExtraFullscreenUIFlags() { int flags = 0; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { flags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; flags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } return flags; } }