1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.chrome.browser.infobar;
7 import android.animation.ObjectAnimator;
8 import android.app.Activity;
9 import android.graphics.Canvas;
10 import android.view.Gravity;
11 import android.view.MotionEvent;
12 import android.view.View;
13 import android.view.ViewGroup;
14 import android.widget.FrameLayout;
15 import android.widget.LinearLayout;
17 import com.google.common.annotations.VisibleForTesting;
19 import org.chromium.base.ApiCompatibilityUtils;
20 import org.chromium.base.CalledByNative;
21 import org.chromium.content.browser.DeviceUtils;
22 import org.chromium.ui.UiUtils;
24 import java.util.ArrayDeque;
25 import java.util.ArrayList;
26 import java.util.Iterator;
27 import java.util.LinkedList;
31 * A container for all the infobars of a specific tab.
32 * Note that infobars creation can be initiated from Java of from native code.
33 * When initiated from native code, special code is needed to keep the Java and native infobar in
34 * sync, see NativeInfoBar.
36 public class InfoBarContainer extends LinearLayout {
37 private static final String TAG = "InfoBarContainer";
38 private static final long REATTACH_FADE_IN_MS = 250;
40 public interface InfoBarAnimationListener {
42 * Notifies the subscriber when an animation is completed.
44 void notifyAnimationFinished(int animationType);
47 private static class InfoBarTransitionInfo {
48 // InfoBar being animated.
49 public InfoBar target;
51 // View to replace the current View shown by the ContentWrapperView.
54 // Which type of animation needs to be performed.
55 public int animationType;
57 public InfoBarTransitionInfo(InfoBar bar, View view, int type) {
58 assert type >= AnimationHelper.ANIMATION_TYPE_SHOW;
59 assert type < AnimationHelper.ANIMATION_TYPE_BOUNDARY;
67 private InfoBarAnimationListener mAnimationListener;
69 // Native InfoBarContainer pointer which will be set by nativeInit()
70 private int mNativeInfoBarContainer;
72 private final Activity mActivity;
74 private final AutoLoginDelegate mAutoLoginDelegate;
76 // Whether the infobar are shown on top (below the location bar) or at the bottom of the screen.
77 private final boolean mInfoBarsOnTop;
79 // The list of all infobars in this container, regardless of whether they've been shown yet.
80 private final ArrayList<InfoBar> mInfoBars = new ArrayList<InfoBar>();
82 // We only animate changing infobars one at a time.
83 private final ArrayDeque<InfoBarTransitionInfo> mInfoBarTransitions;
85 // Animation currently moving InfoBars around.
86 private AnimationHelper mAnimation;
87 private final FrameLayout mAnimationSizer;
89 // True when this container has been emptied and its native counterpart has been destroyed.
90 private boolean mDestroyed = false;
92 // The id of the tab associated with us. Set to TabBase.INVALID_TAB_ID if no tab is associated.
95 // Parent view that contains us.
96 private ViewGroup mParentView;
98 public InfoBarContainer(Activity activity, AutoLoginProcessor autoLoginProcessor,
99 int tabId, ViewGroup parentView, int nativeWebContents) {
101 setOrientation(LinearLayout.VERTICAL);
102 mAnimationListener = null;
103 mInfoBarTransitions = new ArrayDeque<InfoBarTransitionInfo>();
105 mAutoLoginDelegate = new AutoLoginDelegate(autoLoginProcessor, activity);
106 mActivity = activity;
108 mParentView = parentView;
110 mAnimationSizer = new FrameLayout(activity);
111 mAnimationSizer.setVisibility(INVISIBLE);
113 // The tablet has the infobars below the location bar. On the phone they are at the bottom.
114 mInfoBarsOnTop = DeviceUtils.isTablet(activity);
115 setGravity(determineGravity());
117 // Chromium's InfoBarContainer may add an InfoBar immediately during this initialization
118 // call, so make sure everything in the InfoBarContainer is completely ready beforehand.
119 mNativeInfoBarContainer = nativeInit(nativeWebContents, mAutoLoginDelegate);
122 public void setAnimationListener(InfoBarAnimationListener listener) {
123 mAnimationListener = listener;
127 public InfoBarAnimationListener getAnimationListener() {
128 return mAnimationListener;
132 public boolean areInfoBarsOnTop() {
133 return mInfoBarsOnTop;
137 public boolean onInterceptTouchEvent(MotionEvent ev) {
138 // Trap any attempts to fiddle with the Views while we're animating.
139 return mAnimation != null;
143 public boolean onTouchEvent(MotionEvent event) {
144 // Consume all motion events so they do not reach the ContentView.
148 private void addToParentView() {
149 if (mParentView != null && mParentView.indexOfChild(this) == -1) {
150 mParentView.addView(this, createLayoutParams());
154 private int determineGravity() {
155 return mInfoBarsOnTop ? Gravity.TOP : Gravity.BOTTOM;
158 private FrameLayout.LayoutParams createLayoutParams() {
159 return new FrameLayout.LayoutParams(
160 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, determineGravity());
163 public void removeFromParentView() {
164 if (getParent() != null) {
165 ((ViewGroup) getParent()).removeView(this);
170 * Called when the parent {@link android.view.ViewGroup} has changed for
173 public void onParentViewChanged(int tabId, ViewGroup parentView) {
175 mParentView = parentView;
177 if (getParent() != null) {
178 removeFromParentView();
184 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
185 if (mAnimation == null || child != mAnimation.getTarget()) {
186 return super.drawChild(canvas, child, drawingTime);
188 // When infobars are on top, the new infobar Z-order is greater than the previous infobar,
189 // which means it shows on top during the animation. We cannot change the Z-order in the
190 // linear layout, it is driven by the insertion index.
191 // So we simply clip the children to their bounds to make sure the new infobar does not
195 canvas.clipRect(mAnimation.getTarget().getClippingRect());
196 retVal = super.drawChild(canvas, child, drawingTime);
202 protected void onAttachedToWindow() {
203 super.onAttachedToWindow();
204 ObjectAnimator.ofFloat(this, "alpha", 0.f, 1.f).setDuration(REATTACH_FADE_IN_MS).start();
205 setVisibility(VISIBLE);
209 protected void onDetachedFromWindow() {
210 super.onDetachedFromWindow();
211 setVisibility(INVISIBLE);
214 public InfoBar findInfoBar(int nativeInfoBar) {
215 for (InfoBar infoBar : mInfoBars) {
216 if (infoBar.ownsNativeInfoBar(nativeInfoBar)) {
224 * Adds an InfoBar to the view hierarchy.
225 * @param infoBar InfoBar to add to the View hierarchy.
228 public void addInfoBar(InfoBar infoBar) {
230 if (infoBar == null) {
233 if (mInfoBars.contains(infoBar)) {
234 assert false : "Trying to add an info bar that has already been added.";
238 // We add the infobar immediately to mInfoBars but we wait for the animation to end to
239 // notify it's been added, as tests rely on this notification but expects the infobar view
240 // to be available when they get the notification.
241 mInfoBars.add(infoBar);
242 infoBar.setContext(mActivity);
243 infoBar.setInfoBarContainer(this);
245 enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_SHOW);
249 * Returns the latest InfoBarTransitionInfo that deals with the given InfoBar.
250 * @param toFind InfoBar that we're looking for.
252 public InfoBarTransitionInfo findLastTransitionForInfoBar(InfoBar toFind) {
253 Iterator<InfoBarTransitionInfo> iterator = mInfoBarTransitions.descendingIterator();
254 while (iterator.hasNext()) {
255 InfoBarTransitionInfo info = iterator.next();
256 if (info.target == toFind) return info;
262 * Animates swapping out the current View in the {@code infoBar} with {@code toShow} without
263 * destroying or dismissing the entire InfoBar.
264 * @param infoBar InfoBar that is having its content replaced.
265 * @param toShow View representing the InfoBar's new contents.
267 public void swapInfoBarViews(InfoBar infoBar, View toShow) {
270 if (!mInfoBars.contains(infoBar)) {
271 assert false : "Trying to swap an InfoBar that is not in this container.";
275 InfoBarTransitionInfo transition = findLastTransitionForInfoBar(infoBar);
276 if (transition != null && transition.toShow == toShow) {
277 assert false : "Tried to enqueue the same swap twice in a row.";
281 enqueueInfoBarAnimation(infoBar, toShow, AnimationHelper.ANIMATION_TYPE_SWAP);
285 * Removes an InfoBar from the view hierarchy.
286 * @param infoBar InfoBar to remove from the View hierarchy.
288 public void removeInfoBar(InfoBar infoBar) {
291 if (!mInfoBars.remove(infoBar)) {
292 assert false : "Trying to remove an InfoBar that is not in this container.";
296 // If an InfoBar is told to hide itself before it has a chance to be shown, don't bother
297 // with animating any of it.
298 boolean collapseAnimations = false;
299 ArrayDeque<InfoBarTransitionInfo> transitionCopy =
300 new ArrayDeque<InfoBarTransitionInfo>(mInfoBarTransitions);
301 for (InfoBarTransitionInfo info : transitionCopy) {
302 if (info.target == infoBar) {
303 if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
304 // We can assert that two attempts to show the same InfoBar won't be in the
305 // deque simultaneously because of the check in addInfoBar().
306 assert !collapseAnimations;
307 collapseAnimations = true;
309 if (collapseAnimations) {
310 mInfoBarTransitions.remove(info);
315 if (!collapseAnimations) {
316 enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_HIDE);
321 * Enqueue a new animation to run and kicks off the animation sequence.
323 private void enqueueInfoBarAnimation(InfoBar infoBar, View toShow, int animationType) {
324 InfoBarTransitionInfo info = new InfoBarTransitionInfo(infoBar, toShow, animationType);
325 mInfoBarTransitions.add(info);
326 processPendingInfoBars();
330 protected void onLayout(boolean changed, int l, int t, int r, int b) {
331 // Hide the infobars when the keyboard is showing.
332 boolean isShowing = (getVisibility() == View.VISIBLE);
333 if (UiUtils.isKeyboardShowing(mActivity, this)) {
335 setVisibility(View.INVISIBLE);
339 setVisibility(View.VISIBLE);
342 super.onLayout(changed, l, t, r, b);
346 * @return True when this container has been emptied and its native counterpart has been
349 public boolean hasBeenDestroyed() {
353 private void processPendingInfoBars() {
354 if (mAnimation != null || mInfoBarTransitions.isEmpty()) return;
356 // Start animating what has to be animated.
357 InfoBarTransitionInfo info = mInfoBarTransitions.remove();
358 View toShow = info.toShow;
359 ContentWrapperView targetView;
363 if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
364 targetView = info.target.getContentWrapper(true);
365 assert mInfoBars.contains(info.target);
366 toShow = targetView.detachCurrentView();
367 addView(targetView, mInfoBarsOnTop ? getChildCount() : 0,
368 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
370 targetView = info.target.getContentWrapper(false);
373 // Kick off the animation.
374 mAnimation = new AnimationHelper(this, targetView, info.target, toShow, info.animationType);
378 // Called by the tab when it has started loading a new page.
379 public void onPageStarted(String url) {
380 LinkedList<InfoBar> barsToRemove = new LinkedList<InfoBar>();
382 for (InfoBar infoBar : mInfoBars) {
383 if (infoBar.shouldExpire(url)) {
384 barsToRemove.add(infoBar);
388 for (InfoBar infoBar : barsToRemove) {
389 infoBar.dismissJavaOnlyInfoBar();
394 * Returns the id of the tab we are associated with.
396 public int getTabId() {
400 public void destroy() {
403 if (mNativeInfoBarContainer != 0) {
404 nativeDestroy(mNativeInfoBarContainer);
406 mInfoBarTransitions.clear();
410 * @return all of the InfoBars held in this container.
413 public ArrayList<InfoBar> getInfoBars() {
418 * Dismisses all {@link AutoLoginInfoBar}s in this {@link InfoBarContainer} that are for
419 * {@code accountName} and {@code authToken}. This also resets all {@link InfoBar}s that are
420 * for a different request.
421 * @param accountName The name of the account request is being accessed for.
422 * @param authToken The authentication token access is being requested for.
423 * @param success Whether or not the authentication attempt was successful.
424 * @param result The resulting token for the auto login request (ignored if {@code success} is
427 public void processAutoLogin(String accountName, String authToken, boolean success,
429 mAutoLoginDelegate.dismissAutoLogins(accountName, authToken, success, result);
433 * Dismiss all auto logins infobars without processing any result.
435 public void dismissAutoLoginInfoBars() {
436 mAutoLoginDelegate.dismissAutoLogins("", "", false, "");
439 public void prepareTransition(View toShow) {
440 if (toShow != null) {
441 // In order to animate the addition of the infobar, we need a layout first.
442 // Attach the child to invisible layout so that we can get measurements for it without
443 // moving everything in the real container.
444 ViewGroup parent = (ViewGroup) toShow.getParent();
445 if (parent != null) parent.removeView(toShow);
447 assert mAnimationSizer.getParent() == null;
448 mParentView.addView(mAnimationSizer, createLayoutParams());
449 mAnimationSizer.addView(toShow, 0,
450 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
451 mAnimationSizer.requestLayout();
455 public void startTransition() {
456 if (mInfoBarsOnTop) {
457 // We need to clip this view to its bounds while it is animated because the layout's
458 // z-ordering puts it on top of other infobars as it's being animated.
459 ApiCompatibilityUtils.postInvalidateOnAnimation(this);
464 * Finishes off whatever animation is running.
466 public void finishTransition() {
467 assert mAnimation != null;
469 // If the InfoBar was hidden, get rid of its View entirely.
470 if (mAnimation.getAnimationType() == AnimationHelper.ANIMATION_TYPE_HIDE) {
471 removeView(mAnimation.getTarget());
474 // Reset all translations and put everything where they need to be.
475 for (int i = 0; i < getChildCount(); ++i) {
476 View view = getChildAt(i);
477 view.setTranslationY(0);
481 // If there are no infobars shown, there is no need to keep the infobar container in the
483 if (getChildCount() == 0) {
484 removeFromParentView();
487 if (mAnimationSizer.getParent() != null) {
488 ((ViewGroup) mAnimationSizer.getParent()).removeView(mAnimationSizer);
491 // Notify interested parties and move on to the next animation.
492 if (mAnimationListener != null) {
493 mAnimationListener.notifyAnimationFinished(mAnimation.getAnimationType());
496 processPendingInfoBars();
500 * Searches a given view's child views for an instance of {@link InfoBarContainer}.
502 * @param parentView View to be searched for
503 * @return {@link InfoBarContainer} instance if it's one of the child views;
504 * otherwise {@code null}.
506 public static InfoBarContainer childViewOf(ViewGroup parentView) {
507 for (int i = 0; i < parentView.getChildCount(); i++) {
508 if (parentView.getChildAt(i) instanceof InfoBarContainer) {
509 return (InfoBarContainer) parentView.getChildAt(i);
515 public int getNative() {
516 return mNativeInfoBarContainer;
519 private native int nativeInit(int webContentsPtr, AutoLoginDelegate autoLoginDelegate);
521 private native void nativeDestroy(int nativeInfoBarContainerAndroid);