| // 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.animation.ObjectAnimator; |
| import android.app.Activity; |
| import android.graphics.Canvas; |
| import android.view.Gravity; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.FrameLayout; |
| import android.widget.LinearLayout; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.CalledByNative; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.ui.UiUtils; |
| import org.chromium.ui.base.DeviceFormFactor; |
| |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| |
| |
| /** |
| * 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 LinearLayout { |
| private static final String TAG = "InfoBarContainer"; |
| private static final long REATTACH_FADE_IN_MS = 250; |
| |
| /** |
| * A listener for the InfoBar animation. |
| */ |
| public interface InfoBarAnimationListener { |
| /** |
| * Notifies the subscriber when an animation is completed. |
| */ |
| void notifyAnimationFinished(int animationType); |
| } |
| |
| private static class InfoBarTransitionInfo { |
| // InfoBar being animated. |
| public InfoBar target; |
| |
| // View to replace the current View shown by the ContentWrapperView. |
| public View toShow; |
| |
| // Which type of animation needs to be performed. |
| public int animationType; |
| |
| public InfoBarTransitionInfo(InfoBar bar, View view, int type) { |
| assert type >= AnimationHelper.ANIMATION_TYPE_SHOW; |
| assert type < AnimationHelper.ANIMATION_TYPE_BOUNDARY; |
| |
| target = bar; |
| toShow = view; |
| animationType = type; |
| } |
| } |
| |
| private InfoBarAnimationListener mAnimationListener; |
| |
| // Native InfoBarContainer pointer which will be set by nativeInit() |
| private long mNativeInfoBarContainer; |
| |
| private final Activity mActivity; |
| |
| private final AutoLoginDelegate mAutoLoginDelegate; |
| |
| // Whether the infobar are shown on top (below the location bar) or at the bottom of the screen. |
| private final boolean mInfoBarsOnTop; |
| |
| // The list of all infobars in this container, regardless of whether they've been shown yet. |
| private final ArrayList<InfoBar> mInfoBars = new ArrayList<InfoBar>(); |
| |
| // We only animate changing infobars one at a time. |
| private final ArrayDeque<InfoBarTransitionInfo> mInfoBarTransitions; |
| |
| // Animation currently moving InfoBars around. |
| private AnimationHelper mAnimation; |
| private final FrameLayout mAnimationSizer; |
| |
| // 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 us. |
| private ViewGroup mParentView; |
| |
| public InfoBarContainer(Activity activity, AutoLoginProcessor autoLoginProcessor, |
| int tabId, ViewGroup parentView, WebContents webContents) { |
| super(activity); |
| setOrientation(LinearLayout.VERTICAL); |
| mAnimationListener = null; |
| mInfoBarTransitions = new ArrayDeque<InfoBarTransitionInfo>(); |
| |
| mAutoLoginDelegate = new AutoLoginDelegate(autoLoginProcessor, activity); |
| mActivity = activity; |
| mTabId = tabId; |
| mParentView = parentView; |
| |
| mAnimationSizer = new FrameLayout(activity); |
| mAnimationSizer.setVisibility(INVISIBLE); |
| |
| // The tablet has the infobars below the location bar. On the phone they are at the bottom. |
| mInfoBarsOnTop = DeviceFormFactor.isTablet(activity); |
| setGravity(determineGravity()); |
| |
| // 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(webContents, mAutoLoginDelegate); |
| } |
| |
| public void setAnimationListener(InfoBarAnimationListener listener) { |
| mAnimationListener = listener; |
| } |
| |
| @VisibleForTesting |
| public InfoBarAnimationListener getAnimationListener() { |
| return mAnimationListener; |
| } |
| |
| |
| public boolean areInfoBarsOnTop() { |
| return mInfoBarsOnTop; |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| // Trap any attempts to fiddle with the Views while we're animating. |
| return mAnimation != null; |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| // Consume all motion events so they do not reach the ContentView. |
| return true; |
| } |
| |
| private void addToParentView() { |
| if (mParentView != null && mParentView.indexOfChild(this) == -1) { |
| mParentView.addView(this, createLayoutParams()); |
| } |
| } |
| |
| private int determineGravity() { |
| return mInfoBarsOnTop ? Gravity.TOP : Gravity.BOTTOM; |
| } |
| |
| private FrameLayout.LayoutParams createLayoutParams() { |
| return new FrameLayout.LayoutParams( |
| LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, determineGravity()); |
| } |
| |
| public void removeFromParentView() { |
| if (getParent() != null) { |
| ((ViewGroup) getParent()).removeView(this); |
| } |
| } |
| |
| /** |
| * Called when the parent {@link android.view.ViewGroup} has changed for |
| * this container. |
| */ |
| public void onParentViewChanged(int tabId, ViewGroup parentView) { |
| mTabId = tabId; |
| mParentView = parentView; |
| |
| removeFromParentView(); |
| addToParentView(); |
| } |
| |
| @Override |
| protected boolean drawChild(Canvas canvas, View child, long drawingTime) { |
| if (mAnimation == null || child != mAnimation.getTarget()) { |
| return super.drawChild(canvas, child, drawingTime); |
| } |
| // When infobars are on top, the new infobar Z-order is greater than the previous infobar, |
| // which means it shows on top during the animation. We cannot change the Z-order in the |
| // linear layout, it is driven by the insertion index. |
| // So we simply clip the children to their bounds to make sure the new infobar does not |
| // paint over. |
| boolean retVal; |
| canvas.save(); |
| canvas.clipRect(mAnimation.getTarget().getClippingRect()); |
| retVal = super.drawChild(canvas, child, drawingTime); |
| canvas.restore(); |
| return retVal; |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| ObjectAnimator.ofFloat(this, "alpha", 0.f, 1.f).setDuration(REATTACH_FADE_IN_MS).start(); |
| setVisibility(VISIBLE); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| setVisibility(INVISIBLE); |
| } |
| |
| /** |
| * Adds an InfoBar to the view hierarchy. |
| * @param infoBar InfoBar to add to the View hierarchy. |
| */ |
| @CalledByNative |
| public 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; |
| } |
| |
| // 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(mActivity); |
| infoBar.setInfoBarContainer(this); |
| |
| enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_SHOW); |
| } |
| |
| /** |
| * Returns the latest InfoBarTransitionInfo that deals with the given InfoBar. |
| * @param toFind InfoBar that we're looking for. |
| */ |
| public InfoBarTransitionInfo findLastTransitionForInfoBar(InfoBar toFind) { |
| Iterator<InfoBarTransitionInfo> iterator = mInfoBarTransitions.descendingIterator(); |
| while (iterator.hasNext()) { |
| InfoBarTransitionInfo info = iterator.next(); |
| if (info.target == toFind) return info; |
| } |
| return null; |
| } |
| |
| /** |
| * Animates swapping out the current View in the {@code infoBar} with {@code toShow} without |
| * destroying or dismissing the entire InfoBar. |
| * @param infoBar InfoBar that is having its content replaced. |
| * @param toShow View representing the InfoBar's new contents. |
| */ |
| public void swapInfoBarViews(InfoBar infoBar, View toShow) { |
| assert !mDestroyed; |
| |
| if (!mInfoBars.contains(infoBar)) { |
| assert false : "Trying to swap an InfoBar that is not in this container."; |
| return; |
| } |
| |
| InfoBarTransitionInfo transition = findLastTransitionForInfoBar(infoBar); |
| if (transition != null && transition.toShow == toShow) { |
| assert false : "Tried to enqueue the same swap twice in a row."; |
| return; |
| } |
| |
| enqueueInfoBarAnimation(infoBar, toShow, AnimationHelper.ANIMATION_TYPE_SWAP); |
| } |
| |
| /** |
| * Removes an InfoBar from the view hierarchy. |
| * @param infoBar InfoBar to remove from the View hierarchy. |
| */ |
| public void removeInfoBar(InfoBar infoBar) { |
| assert !mDestroyed; |
| |
| if (!mInfoBars.remove(infoBar)) { |
| assert false : "Trying to remove an InfoBar that is not in this container."; |
| return; |
| } |
| |
| // If an InfoBar is told to hide itself before it has a chance to be shown, don't bother |
| // with animating any of it. |
| boolean collapseAnimations = false; |
| ArrayDeque<InfoBarTransitionInfo> transitionCopy = |
| new ArrayDeque<InfoBarTransitionInfo>(mInfoBarTransitions); |
| for (InfoBarTransitionInfo info : transitionCopy) { |
| if (info.target == infoBar) { |
| if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) { |
| // We can assert that two attempts to show the same InfoBar won't be in the |
| // deque simultaneously because of the check in addInfoBar(). |
| assert !collapseAnimations; |
| collapseAnimations = true; |
| } |
| if (collapseAnimations) { |
| mInfoBarTransitions.remove(info); |
| } |
| } |
| } |
| |
| if (!collapseAnimations) { |
| enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_HIDE); |
| } |
| } |
| |
| /** |
| * Enqueue a new animation to run and kicks off the animation sequence. |
| */ |
| private void enqueueInfoBarAnimation(InfoBar infoBar, View toShow, int animationType) { |
| InfoBarTransitionInfo info = new InfoBarTransitionInfo(infoBar, toShow, animationType); |
| mInfoBarTransitions.add(info); |
| processPendingInfoBars(); |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| // Hide the infobars when the keyboard is showing. |
| boolean isShowing = (getVisibility() == View.VISIBLE); |
| if (UiUtils.isKeyboardShowing(mActivity, this)) { |
| if (isShowing) { |
| setVisibility(View.INVISIBLE); |
| } |
| } else { |
| if (!isShowing) { |
| setVisibility(View.VISIBLE); |
| } |
| } |
| super.onLayout(changed, l, t, r, b); |
| } |
| |
| /** |
| * @return True when this container has been emptied and its native counterpart has been |
| * destroyed. |
| */ |
| public boolean hasBeenDestroyed() { |
| return mDestroyed; |
| } |
| |
| private void processPendingInfoBars() { |
| if (mAnimation != null || mInfoBarTransitions.isEmpty()) return; |
| |
| // Start animating what has to be animated. |
| InfoBarTransitionInfo info = mInfoBarTransitions.remove(); |
| View toShow = info.toShow; |
| ContentWrapperView targetView; |
| |
| addToParentView(); |
| |
| if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) { |
| targetView = info.target.getContentWrapper(true); |
| assert mInfoBars.contains(info.target); |
| toShow = targetView.detachCurrentView(); |
| addView(targetView, mInfoBarsOnTop ? getChildCount() : 0, |
| new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); |
| } else { |
| targetView = info.target.getContentWrapper(false); |
| } |
| |
| // Kick off the animation. |
| mAnimation = new AnimationHelper(this, targetView, info.target, toShow, info.animationType); |
| mAnimation.start(); |
| } |
| |
| // Called by the tab when it has started loading a new page. |
| public void onPageStarted(String url) { |
| LinkedList<InfoBar> barsToRemove = new LinkedList<InfoBar>(); |
| |
| for (InfoBar infoBar : mInfoBars) { |
| if (infoBar.shouldExpire(url)) { |
| barsToRemove.add(infoBar); |
| } |
| } |
| |
| for (InfoBar infoBar : barsToRemove) { |
| infoBar.dismissJavaOnlyInfoBar(); |
| } |
| } |
| |
| /** |
| * Returns the id of the tab we are associated with. |
| */ |
| public int getTabId() { |
| return mTabId; |
| } |
| |
| public void destroy() { |
| mDestroyed = true; |
| removeAllViews(); |
| if (mNativeInfoBarContainer != 0) { |
| nativeDestroy(mNativeInfoBarContainer); |
| } |
| mInfoBarTransitions.clear(); |
| } |
| |
| /** |
| * @return all of the InfoBars held in this container. |
| */ |
| @VisibleForTesting |
| public ArrayList<InfoBar> getInfoBars() { |
| return mInfoBars; |
| } |
| |
| /** |
| * Dismisses all {@link AutoLoginInfoBar}s in this {@link InfoBarContainer} that are for |
| * {@code accountName} and {@code authToken}. This also resets all {@link InfoBar}s that are |
| * for a different request. |
| * @param accountName The name of the account request is being accessed for. |
| * @param authToken The authentication token access is being requested for. |
| * @param success Whether or not the authentication attempt was successful. |
| * @param result The resulting token for the auto login request (ignored if {@code success} is |
| * {@code false}. |
| */ |
| public void processAutoLogin(String accountName, String authToken, boolean success, |
| String result) { |
| mAutoLoginDelegate.dismissAutoLogins(accountName, authToken, success, result); |
| } |
| |
| /** |
| * Dismiss all auto logins infobars without processing any result. |
| */ |
| public void dismissAutoLoginInfoBars() { |
| mAutoLoginDelegate.dismissAutoLogins("", "", false, ""); |
| } |
| |
| public void prepareTransition(View toShow) { |
| if (toShow != null) { |
| // In order to animate the addition of the infobar, we need a layout first. |
| // Attach the child to invisible layout so that we can get measurements for it without |
| // moving everything in the real container. |
| ViewGroup parent = (ViewGroup) toShow.getParent(); |
| if (parent != null) parent.removeView(toShow); |
| |
| assert mAnimationSizer.getParent() == null; |
| mParentView.addView(mAnimationSizer, createLayoutParams()); |
| mAnimationSizer.addView(toShow, 0, |
| new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); |
| mAnimationSizer.requestLayout(); |
| } |
| } |
| |
| public void startTransition() { |
| if (mInfoBarsOnTop) { |
| // We need to clip this view to its bounds while it is animated because the layout's |
| // z-ordering puts it on top of other infobars as it's being animated. |
| ApiCompatibilityUtils.postInvalidateOnAnimation(this); |
| } |
| } |
| |
| /** |
| * Finishes off whatever animation is running. |
| */ |
| public void finishTransition() { |
| assert mAnimation != null; |
| |
| // If the InfoBar was hidden, get rid of its View entirely. |
| if (mAnimation.getAnimationType() == AnimationHelper.ANIMATION_TYPE_HIDE) { |
| removeView(mAnimation.getTarget()); |
| } |
| |
| // Reset all translations and put everything where they need to be. |
| for (int i = 0; i < getChildCount(); ++i) { |
| View view = getChildAt(i); |
| view.setTranslationY(0); |
| } |
| requestLayout(); |
| |
| // If there are no infobars shown, there is no need to keep the infobar container in the |
| // view hierarchy. |
| if (getChildCount() == 0) { |
| removeFromParentView(); |
| } |
| |
| if (mAnimationSizer.getParent() != null) { |
| ((ViewGroup) mAnimationSizer.getParent()).removeView(mAnimationSizer); |
| } |
| |
| // Notify interested parties and move on to the next animation. |
| if (mAnimationListener != null) { |
| mAnimationListener.notifyAnimationFinished(mAnimation.getAnimationType()); |
| } |
| mAnimation = null; |
| processPendingInfoBars(); |
| } |
| |
| /** |
| * Searches a given view's child views for an instance of {@link InfoBarContainer}. |
| * |
| * @param parentView View to be searched for |
| * @return {@link InfoBarContainer} instance if it's one of the child views; |
| * otherwise {@code null}. |
| */ |
| public static InfoBarContainer childViewOf(ViewGroup parentView) { |
| for (int i = 0; i < parentView.getChildCount(); i++) { |
| if (parentView.getChildAt(i) instanceof InfoBarContainer) { |
| return (InfoBarContainer) parentView.getChildAt(i); |
| } |
| } |
| return null; |
| } |
| |
| public long getNative() { |
| return mNativeInfoBarContainer; |
| } |
| |
| private native long nativeInit(WebContents webContents, AutoLoginDelegate autoLoginDelegate); |
| |
| private native void nativeDestroy(long nativeInfoBarContainerAndroid); |
| } |