// Copyright 2013 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.infobar; import android.content.Context; import android.view.Gravity; import android.view.View; import android.widget.FrameLayout; import org.chromium.base.ObserverList; import org.chromium.base.VisibleForTesting; import org.chromium.base.annotations.CalledByNative; import org.chromium.chrome.browser.banners.SwipableOverlayView; import org.chromium.chrome.browser.tab.EmptyTabObserver; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.TabContentViewParent; import org.chromium.chrome.browser.tab.TabObserver; import org.chromium.content.browser.ContentViewCore; import org.chromium.content_public.browser.WebContents; import org.chromium.ui.UiUtils; import org.chromium.ui.base.DeviceFormFactor; import java.util.ArrayList; /** * A container for all the infobars of a specific tab. * Note that infobars creation can be initiated from Java of from native code. * When initiated from native code, special code is needed to keep the Java and native infobar in * sync, see NativeInfoBar. */ public class InfoBarContainer extends SwipableOverlayView { private static final String TAG = "InfoBarContainer"; /** Top margin, including the toolbar and tabstrip height and 48dp of web contents. */ private static final int TOP_MARGIN_PHONE_DP = 104; private static final int TOP_MARGIN_TABLET_DP = 144; /** Length of the animation to fade the InfoBarContainer back into View. */ private static final long REATTACH_FADE_IN_MS = 250; /** Whether or not the InfoBarContainer is allowed to hide when the user scrolls. */ private static boolean sIsAllowedToAutoHide = true; /** * A listener for the InfoBar animations. */ public interface InfoBarAnimationListener { public static final int ANIMATION_TYPE_SHOW = 0; public static final int ANIMATION_TYPE_SWAP = 1; public static final int ANIMATION_TYPE_HIDE = 2; /** * Notifies the subscriber when an animation is completed. */ void notifyAnimationFinished(int animationType); } /** * An observer that is notified of changes to a {@link InfoBarContainer} object. */ public interface InfoBarContainerObserver { /** * Called when an {@link InfoBar} is about to be added (before the animation). * @param container The notifying {@link InfoBarContainer} * @param infoBar An {@link InfoBar} being added * @param isFirst Whether the infobar container was empty */ void onAddInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isFirst); /** * Called when an {@link InfoBar} is about to be removed (before the animation). * @param container The notifying {@link InfoBarContainer} * @param infoBar An {@link InfoBar} being removed * @param isLast Whether the infobar container is going to be empty */ void onRemoveInfoBar(InfoBarContainer container, InfoBar infoBar, boolean isLast); /** * Called when the InfobarContainer is attached to the window. * @param hasInfobars True if infobar container has infobars to show. */ void onInfoBarContainerAttachedToWindow(boolean hasInfobars); } /** Resets the state of the InfoBarContainer when the user navigates. */ private final TabObserver mTabObserver = new EmptyTabObserver() { @Override public void onDidNavigateMainFrame(Tab tab, String url, String baseUrl, boolean isNavigationToDifferentPage, boolean isFragmentNavigation, int statusCode) { setIsObscuredByOtherView(false); } @Override public void onReparentingFinished(Tab tab) { for (InfoBar infobar : mInfoBars) { infobar.onTabReparented(tab); } } }; private final InfoBarContainerLayout mLayout; /** Native InfoBarContainer pointer which will be set by nativeInit(). */ private final long mNativeInfoBarContainer; /** The list of all InfoBars in this container, regardless of whether they've been shown yet. */ private final ArrayList<InfoBar> mInfoBars = new ArrayList<InfoBar>(); /** True when this container has been emptied and its native counterpart has been destroyed. */ private boolean mDestroyed = false; /** The id of the tab associated with us. Set to Tab.INVALID_TAB_ID if no tab is associated. */ private int mTabId; /** Parent view that contains the InfoBarContainerLayout. */ private TabContentViewParent mParentView; /** Whether or not another View is occupying the same space as this one. */ private boolean mIsObscured; private final ObserverList<InfoBarContainerObserver> mObservers = new ObserverList<InfoBarContainerObserver>(); public InfoBarContainer(Context context, int tabId, TabContentViewParent parentView, Tab tab) { super(context, null); tab.addObserver(mTabObserver); // TODO(newt): move this workaround into the infobar views if/when they're scrollable. // Workaround for http://crbug.com/407149. See explanation in onMeasure() below. setVerticalScrollBarEnabled(false); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM); int topMarginDp = DeviceFormFactor.isTablet(context) ? TOP_MARGIN_TABLET_DP : TOP_MARGIN_PHONE_DP; lp.topMargin = Math.round(topMarginDp * getResources().getDisplayMetrics().density); setLayoutParams(lp); mTabId = tabId; mParentView = parentView; mLayout = new InfoBarContainerLayout(context); addView(mLayout, new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL)); // Chromium's InfoBarContainer may add an InfoBar immediately during this initialization // call, so make sure everything in the InfoBarContainer is completely ready beforehand. mNativeInfoBarContainer = nativeInit(); } /** * Adds an {@link InfoBarContainerObserver}. * @param observer The {@link InfoBarContainerObserver} to add. */ public void addObserver(InfoBarContainerObserver observer) { mObservers.addObserver(observer); } /** * Removes a {@link InfoBarContainerObserver}. * @param observer The {@link InfoBarContainerObserver} to remove. */ public void removeObserver(InfoBarContainerObserver observer) { mObservers.removeObserver(observer); } @Override public void setContentViewCore(ContentViewCore contentViewCore) { super.setContentViewCore(contentViewCore); if (getContentViewCore() != null) { nativeSetWebContents(mNativeInfoBarContainer, contentViewCore.getWebContents()); } } @VisibleForTesting public void setAnimationListener(InfoBarAnimationListener listener) { mLayout.setAnimationListener(listener); } /** * Returns true if any animations are pending or in progress. */ @VisibleForTesting public boolean isAnimating() { return mLayout.isAnimating(); } private void addToParentView() { super.addToParentView(mParentView); } /** * Called when the parent {@link android.view.ViewGroup} has changed for * this container. */ public void onParentViewChanged(int tabId, TabContentViewParent parentView) { mTabId = tabId; mParentView = parentView; removeFromParentView(); addToParentView(); } /** * Adds an InfoBar to the view hierarchy. * @param infoBar InfoBar to add to the View hierarchy. */ @CalledByNative private void addInfoBar(InfoBar infoBar) { assert !mDestroyed; if (infoBar == null) { return; } if (mInfoBars.contains(infoBar)) { assert false : "Trying to add an info bar that has already been added."; return; } addToParentView(); // We notify observers immediately (before the animation starts). for (InfoBarContainerObserver observer : mObservers) { observer.onAddInfoBar(this, infoBar, mInfoBars.isEmpty()); } // We add the infobar immediately to mInfoBars but we wait for the animation to end to // notify it's been added, as tests rely on this notification but expects the infobar view // to be available when they get the notification. mInfoBars.add(infoBar); infoBar.setContext(getContext()); infoBar.setInfoBarContainer(this); infoBar.createView(); mLayout.addInfoBar(infoBar); } /** * Notifies that an infobar's View ({@link InfoBar#getView}) has changed. If the infobar is * visible, a view swapping animation will be run. */ public void notifyInfoBarViewChanged() { assert !mDestroyed; mLayout.notifyInfoBarViewChanged(); } /** * Called by {@link InfoBar} to remove itself from the view hierarchy. * * @param infoBar InfoBar to remove from the View hierarchy. */ void removeInfoBar(InfoBar infoBar) { assert !mDestroyed; if (!mInfoBars.remove(infoBar)) { assert false : "Trying to remove an InfoBar that is not in this container."; return; } // Notify observers immediately, before any animations begin. for (InfoBarContainerObserver observer : mObservers) { observer.onRemoveInfoBar(this, infoBar, mInfoBars.isEmpty()); } mLayout.removeInfoBar(infoBar); } /** * @return True when this container has been emptied and its native counterpart has been * destroyed. */ public boolean hasBeenDestroyed() { return mDestroyed; } public void destroy() { mDestroyed = true; if (mNativeInfoBarContainer != 0) { nativeDestroy(mNativeInfoBarContainer); } } /** * @return all of the InfoBars held in this container. */ @VisibleForTesting public ArrayList<InfoBar> getInfoBarsForTesting() { return mInfoBars; } /** * @return True if the container has any InfoBars. */ @CalledByNative public boolean hasInfoBars() { return !mInfoBars.isEmpty(); } /** * @return Pointer to the native InfoBarAndroid object which is currently at the top of the * infobar stack, or 0 if there are no infobars. */ @CalledByNative private long getTopNativeInfoBarPtr() { if (!hasInfoBars()) return 0; return mInfoBars.get(0).getNativeInfoBarPtr(); } /** * Tells this class that a View with higher priority is occupying the same space. * * Causes this View to hide itself until the obscuring View goes away. * * @param isObscured Whether this View is obscured by another one. */ public void setIsObscuredByOtherView(boolean isObscured) { mIsObscured = isObscured; if (isObscured) { setVisibility(View.GONE); } else { setVisibility(View.VISIBLE); } } /** * Sets whether the InfoBarContainer is allowed to auto-hide when the user scrolls the page. * Expected to be called when Touch Exploration is enabled. * @param isAllowed Whether auto-hiding is allowed. */ public static void setIsAllowedToAutoHide(boolean isAllowed) { sIsAllowedToAutoHide = isAllowed; } @Override protected boolean isAllowedToAutoHide() { return sIsAllowedToAutoHide; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (!mIsObscured) { setVisibility(VISIBLE); setAlpha(0f); animate().alpha(1f).setDuration(REATTACH_FADE_IN_MS); } // Notify observers that the container has attached to the window. for (InfoBarContainerObserver observer : mObservers) { observer.onInfoBarContainerAttachedToWindow(!mInfoBars.isEmpty()); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // Hide the View when the keyboard is showing. boolean isShowing = (getVisibility() == View.VISIBLE); if (UiUtils.isKeyboardShowing(getContext(), InfoBarContainer.this)) { if (isShowing) { // Set to invisible (instead of gone) so that onLayout() will be called when the // keyboard is dismissed. setVisibility(View.INVISIBLE); } } else { if (!isShowing && !mIsObscured) { setVisibility(View.VISIBLE); } } super.onLayout(changed, l, t, r, b); } private native long nativeInit(); private native void nativeSetWebContents( long nativeInfoBarContainerAndroid, WebContents webContents); private native void nativeDestroy(long nativeInfoBarContainerAndroid); }