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.graphics.Paint;
11 import android.view.Gravity;
12 import android.view.MotionEvent;
13 import android.view.View;
14 import android.view.ViewGroup;
15 import android.widget.FrameLayout;
16 import android.widget.LinearLayout;
17 import android.widget.ScrollView;
19 import org.chromium.base.CalledByNative;
20 import org.chromium.base.VisibleForTesting;
21 import org.chromium.chrome.R;
22 import org.chromium.chrome.browser.EmptyTabObserver;
23 import org.chromium.chrome.browser.Tab;
24 import org.chromium.chrome.browser.TabObserver;
25 import org.chromium.content_public.browser.WebContents;
26 import org.chromium.ui.UiUtils;
27 import org.chromium.ui.base.DeviceFormFactor;
29 import java.util.ArrayDeque;
30 import java.util.ArrayList;
31 import java.util.Iterator;
32 import java.util.LinkedList;
36 * A container for all the infobars of a specific tab.
37 * Note that infobars creation can be initiated from Java of from native code.
38 * When initiated from native code, special code is needed to keep the Java and native infobar in
39 * sync, see NativeInfoBar.
41 public class InfoBarContainer extends ScrollView {
42 private static final String TAG = "InfoBarContainer";
43 private static final long REATTACH_FADE_IN_MS = 250;
44 private static final int TAB_STRIP_AND_TOOLBAR_HEIGHT_PHONE_DP = 56;
45 private static final int TAB_STRIP_AND_TOOLBAR_HEIGHT_TABLET_DP = 96;
48 * A listener for the InfoBar animation.
50 public interface InfoBarAnimationListener {
52 * Notifies the subscriber when an animation is completed.
54 void notifyAnimationFinished(int animationType);
57 private static class InfoBarTransitionInfo {
58 // InfoBar being animated.
59 public InfoBar target;
61 // View to replace the current View shown by the ContentWrapperView.
64 // Which type of animation needs to be performed.
65 public int animationType;
67 public InfoBarTransitionInfo(InfoBar bar, View view, int type) {
68 assert type >= AnimationHelper.ANIMATION_TYPE_SHOW;
69 assert type < AnimationHelper.ANIMATION_TYPE_BOUNDARY;
77 private InfoBarAnimationListener mAnimationListener;
79 // Native InfoBarContainer pointer which will be set by nativeInit()
80 private long mNativeInfoBarContainer;
82 private final Activity mActivity;
84 private final AutoLoginDelegate mAutoLoginDelegate;
86 // The list of all infobars in this container, regardless of whether they've been shown yet.
87 private final ArrayList<InfoBar> mInfoBars = new ArrayList<InfoBar>();
89 // We only animate changing infobars one at a time.
90 private final ArrayDeque<InfoBarTransitionInfo> mInfoBarTransitions;
92 // Animation currently moving InfoBars around.
93 private AnimationHelper mAnimation;
94 private final FrameLayout mAnimationSizer;
96 // True when this container has been emptied and its native counterpart has been destroyed.
97 private boolean mDestroyed = false;
99 // The id of the tab associated with us. Set to Tab.INVALID_TAB_ID if no tab is associated.
102 // Parent view that contains us.
103 private ViewGroup mParentView;
105 // The LinearLayout that holds the infobars. This is the only child of the InfoBarContainer.
106 private LinearLayout mLinearLayout;
108 // These values are used in onLayout() to keep the infobars fixed to the bottom of the screen
109 // when infobars are added or removed.
111 private int mInnerHeight;
112 private int mDistanceFromBottom;
114 private Paint mTopBorderPaint;
116 // Keeps the infobars from becoming visible when they normally would.
117 private boolean mDoStayInvisible;
118 private TabObserver mTabObserver;
120 public InfoBarContainer(Activity activity, AutoLoginProcessor autoLoginProcessor,
121 int tabId, ViewGroup parentView, WebContents webContents) {
124 // Workaround for http://crbug.com/407149. See explanation in onMeasure() below.
125 setVerticalScrollBarEnabled(false);
127 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
128 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM);
129 int topMarginDp = DeviceFormFactor.isTablet(activity)
130 ? TAB_STRIP_AND_TOOLBAR_HEIGHT_TABLET_DP
131 : TAB_STRIP_AND_TOOLBAR_HEIGHT_PHONE_DP;
132 lp.topMargin = Math.round(topMarginDp * getResources().getDisplayMetrics().density);
135 mLinearLayout = new LinearLayout(activity);
136 mLinearLayout.setOrientation(LinearLayout.VERTICAL);
137 addView(mLinearLayout,
138 new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
140 mAnimationListener = null;
141 mInfoBarTransitions = new ArrayDeque<InfoBarTransitionInfo>();
143 mAutoLoginDelegate = new AutoLoginDelegate(autoLoginProcessor, activity);
144 mActivity = activity;
146 mParentView = parentView;
148 mAnimationSizer = new FrameLayout(activity);
149 mAnimationSizer.setVisibility(INVISIBLE);
151 // Chromium's InfoBarContainer may add an InfoBar immediately during this initialization
152 // call, so make sure everything in the InfoBarContainer is completely ready beforehand.
153 mNativeInfoBarContainer = nativeInit(webContents, mAutoLoginDelegate);
157 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
158 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
160 // Only enable scrollbars when the view is actually scrollable.
161 // This prevents 10-15 frames of jank that would otherwise occur 1.2 seconds after the
162 // InfoBarContainer is attached to the window. See: http://crbug.com/407149
163 boolean canScroll = mLinearLayout.getMeasuredHeight() > getMeasuredHeight();
164 if (canScroll != isVerticalScrollBarEnabled()) {
165 setVerticalScrollBarEnabled(canScroll);
170 * @return The LinearLayout that holds the infobars (i.e. the ContentWrapperViews).
172 LinearLayout getLinearLayout() {
173 return mLinearLayout;
177 public void setAnimationListener(InfoBarAnimationListener listener) {
178 mAnimationListener = listener;
182 public InfoBarAnimationListener getAnimationListener() {
183 return mAnimationListener;
187 public boolean onInterceptTouchEvent(MotionEvent ev) {
188 // Trap any attempts to fiddle with the infobars while we're animating.
189 return super.onInterceptTouchEvent(ev) || mAnimation != null;
192 private void addToParentView() {
193 if (mParentView != null && mParentView.indexOfChild(this) == -1) {
194 mParentView.addView(this);
198 public void removeFromParentView() {
199 if (getParent() != null) {
200 ((ViewGroup) getParent()).removeView(this);
205 * Called when the parent {@link android.view.ViewGroup} has changed for
208 public void onParentViewChanged(int tabId, ViewGroup parentView) {
210 mParentView = parentView;
212 removeFromParentView();
217 * Call with {@code true} when a higher priority bottom element is visible to keep the infobars
218 * from ever becoming visible. Call with {@code false} to restore normal visibility behavior.
219 * @param doStayInvisible Whether the infobars should stay invisible even when they would
220 * normally become visible.
221 * @param tab The current Tab.
223 public void setDoStayInvisible(boolean doStayInvisible, Tab tab) {
224 mDoStayInvisible = doStayInvisible;
225 if (mTabObserver == null) mTabObserver = createTabObserver();
226 if (doStayInvisible) {
227 tab.addObserver(mTabObserver);
229 tab.removeObserver(mTabObserver);
234 * Creates a TabObserver for monitoring a Tab, used to reset internal settings when a
235 * navigation is done.
236 * @return TabObserver that can be used to monitor a Tab.
238 private TabObserver createTabObserver() {
239 return new EmptyTabObserver() {
241 public void onDidNavigateMainFrame(Tab tab, String url, String baseUrl,
242 boolean isNavigationToDifferentPage, boolean isFragmentNavigation,
244 setDoStayInvisible(false, tab);
250 protected void onAttachedToWindow() {
251 super.onAttachedToWindow();
252 if (!mDoStayInvisible) {
253 ObjectAnimator.ofFloat(this, "alpha", 0.f, 1.f).setDuration(REATTACH_FADE_IN_MS)
255 setVisibility(VISIBLE);
260 protected void onDetachedFromWindow() {
261 super.onDetachedFromWindow();
262 setVisibility(INVISIBLE);
266 * Adds an InfoBar to the view hierarchy.
267 * @param infoBar InfoBar to add to the View hierarchy.
270 public void addInfoBar(InfoBar infoBar) {
272 if (infoBar == null) {
275 if (mInfoBars.contains(infoBar)) {
276 assert false : "Trying to add an info bar that has already been added.";
280 // We add the infobar immediately to mInfoBars but we wait for the animation to end to
281 // notify it's been added, as tests rely on this notification but expects the infobar view
282 // to be available when they get the notification.
283 mInfoBars.add(infoBar);
284 infoBar.setContext(mActivity);
285 infoBar.setInfoBarContainer(this);
287 enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_SHOW);
291 * Returns the latest InfoBarTransitionInfo that deals with the given InfoBar.
292 * @param toFind InfoBar that we're looking for.
294 public InfoBarTransitionInfo findLastTransitionForInfoBar(InfoBar toFind) {
295 Iterator<InfoBarTransitionInfo> iterator = mInfoBarTransitions.descendingIterator();
296 while (iterator.hasNext()) {
297 InfoBarTransitionInfo info = iterator.next();
298 if (info.target == toFind) return info;
304 * Animates swapping out the current View in the {@code infoBar} with {@code toShow} without
305 * destroying or dismissing the entire InfoBar.
306 * @param infoBar InfoBar that is having its content replaced.
307 * @param toShow View representing the InfoBar's new contents.
309 public void swapInfoBarViews(InfoBar infoBar, View toShow) {
312 if (!mInfoBars.contains(infoBar)) {
313 assert false : "Trying to swap an InfoBar that is not in this container.";
317 InfoBarTransitionInfo transition = findLastTransitionForInfoBar(infoBar);
318 if (transition != null && transition.toShow == toShow) {
319 assert false : "Tried to enqueue the same swap twice in a row.";
323 enqueueInfoBarAnimation(infoBar, toShow, AnimationHelper.ANIMATION_TYPE_SWAP);
327 * Removes an InfoBar from the view hierarchy.
328 * @param infoBar InfoBar to remove from the View hierarchy.
330 public void removeInfoBar(InfoBar infoBar) {
333 if (!mInfoBars.remove(infoBar)) {
334 assert false : "Trying to remove an InfoBar that is not in this container.";
338 // If an InfoBar is told to hide itself before it has a chance to be shown, don't bother
339 // with animating any of it.
340 boolean collapseAnimations = false;
341 ArrayDeque<InfoBarTransitionInfo> transitionCopy =
342 new ArrayDeque<InfoBarTransitionInfo>(mInfoBarTransitions);
343 for (InfoBarTransitionInfo info : transitionCopy) {
344 if (info.target == infoBar) {
345 if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
346 // We can assert that two attempts to show the same InfoBar won't be in the
347 // deque simultaneously because of the check in addInfoBar().
348 assert !collapseAnimations;
349 collapseAnimations = true;
351 if (collapseAnimations) {
352 mInfoBarTransitions.remove(info);
357 if (!collapseAnimations) {
358 enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_HIDE);
363 * Enqueue a new animation to run and kicks off the animation sequence.
365 private void enqueueInfoBarAnimation(InfoBar infoBar, View toShow, int animationType) {
366 InfoBarTransitionInfo info = new InfoBarTransitionInfo(infoBar, toShow, animationType);
367 mInfoBarTransitions.add(info);
368 processPendingInfoBars();
372 protected void onLayout(boolean changed, int l, int t, int r, int b) {
373 // Hide the infobars when the keyboard is showing.
374 boolean isShowing = (getVisibility() == View.VISIBLE);
375 if (UiUtils.isKeyboardShowing(mActivity, this)) {
377 setVisibility(View.INVISIBLE);
380 if (!isShowing && !mDoStayInvisible) {
381 setVisibility(View.VISIBLE);
384 super.onLayout(changed, l, t, r, b);
386 // Keep the infobars fixed to the bottom of the screen when infobars are added or removed.
387 // Otherwise, infobars jump around when appearing or disappearing on small devices.
388 int newHeight = getHeight();
389 int newInnerHeight = mLinearLayout.getHeight();
390 if (mInnerHeight != newInnerHeight) {
391 int newScrollY = newInnerHeight - newHeight - mDistanceFromBottom;
392 scrollTo(0, newScrollY);
395 mInnerHeight = newInnerHeight;
396 mDistanceFromBottom = mInnerHeight - mHeight - getScrollY();
400 protected void onScrollChanged(int l, int t, int oldl, int oldt) {
401 super.onScrollChanged(l, t, oldl, oldt);
402 mDistanceFromBottom = mInnerHeight - mHeight - getScrollY();
406 protected void dispatchDraw(Canvas canvas) {
407 super.dispatchDraw(canvas);
409 // If the infobars overflow the ScrollView, draw a border at the top of the ScrollView.
410 // This prevents the topmost infobar from blending into the page when fullscreen mode
412 if (getScrollY() != 0) {
413 if (mTopBorderPaint == null) {
414 mTopBorderPaint = new Paint();
415 mTopBorderPaint.setColor(
416 getResources().getColor(R.color.infobar_background_separator));
418 int height = ContentWrapperView.getBoundaryHeight(getContext());
419 canvas.drawRect(0, getScrollY(), getWidth(), getScrollY() + height, mTopBorderPaint);
424 * @return True when this container has been emptied and its native counterpart has been
427 public boolean hasBeenDestroyed() {
431 private void processPendingInfoBars() {
432 if (mAnimation != null || mInfoBarTransitions.isEmpty()) return;
434 // Start animating what has to be animated.
435 InfoBarTransitionInfo info = mInfoBarTransitions.remove();
436 View toShow = info.toShow;
437 ContentWrapperView targetView;
441 if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
442 targetView = info.target.getContentWrapper(true);
443 assert mInfoBars.contains(info.target);
444 toShow = targetView.detachCurrentView();
445 mLinearLayout.addView(targetView, 0,
446 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
448 targetView = info.target.getContentWrapper(false);
451 // Kick off the animation.
452 mAnimation = new AnimationHelper(this, targetView, info.target, toShow, info.animationType);
456 // Called by the tab when it has started loading a new page.
457 public void onPageStarted() {
458 LinkedList<InfoBar> barsToRemove = new LinkedList<InfoBar>();
460 for (InfoBar infoBar : mInfoBars) {
461 if (infoBar.shouldExpire()) {
462 barsToRemove.add(infoBar);
466 for (InfoBar infoBar : barsToRemove) {
467 infoBar.dismissJavaOnlyInfoBar();
472 * Returns the id of the tab we are associated with.
474 public int getTabId() {
478 public void destroy() {
480 mLinearLayout.removeAllViews();
481 if (mNativeInfoBarContainer != 0) {
482 nativeDestroy(mNativeInfoBarContainer);
484 mInfoBarTransitions.clear();
488 * @return all of the InfoBars held in this container.
491 public ArrayList<InfoBar> getInfoBars() {
496 * Dismisses all {@link AutoLoginInfoBar}s in this {@link InfoBarContainer} that are for
497 * {@code accountName} and {@code authToken}. This also resets all {@link InfoBar}s that are
498 * for a different request.
499 * @param accountName The name of the account request is being accessed for.
500 * @param authToken The authentication token access is being requested for.
501 * @param success Whether or not the authentication attempt was successful.
502 * @param result The resulting token for the auto login request (ignored if {@code success} is
505 public void processAutoLogin(String accountName, String authToken, boolean success,
507 mAutoLoginDelegate.dismissAutoLogins(accountName, authToken, success, result);
511 * Dismiss all auto logins infobars without processing any result.
513 public void dismissAutoLoginInfoBars() {
514 mAutoLoginDelegate.dismissAutoLogins("", "", false, "");
517 public void prepareTransition(View toShow) {
518 if (toShow != null) {
519 // In order to animate the addition of the infobar, we need a layout first.
520 // Attach the child to invisible layout so that we can get measurements for it without
521 // moving everything in the real container.
522 ViewGroup parent = (ViewGroup) toShow.getParent();
523 if (parent != null) parent.removeView(toShow);
525 assert mAnimationSizer.getParent() == null;
526 mParentView.addView(mAnimationSizer, new FrameLayout.LayoutParams(
527 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
528 mAnimationSizer.addView(toShow, 0,
529 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
530 mAnimationSizer.requestLayout();
535 * Finishes off whatever animation is running.
537 public void finishTransition() {
538 assert mAnimation != null;
540 // If the InfoBar was hidden, get rid of its View entirely.
541 if (mAnimation.getAnimationType() == AnimationHelper.ANIMATION_TYPE_HIDE) {
542 mLinearLayout.removeView(mAnimation.getTarget());
545 // Reset all translations and put everything where they need to be.
546 for (int i = 0; i < mLinearLayout.getChildCount(); ++i) {
547 View view = mLinearLayout.getChildAt(i);
548 view.setTranslationY(0);
552 // If there are no infobars shown, there is no need to keep the infobar container in the
554 if (mLinearLayout.getChildCount() == 0) {
555 removeFromParentView();
558 if (mAnimationSizer.getParent() != null) {
559 ((ViewGroup) mAnimationSizer.getParent()).removeView(mAnimationSizer);
562 // Notify interested parties and move on to the next animation.
563 if (mAnimationListener != null) {
564 mAnimationListener.notifyAnimationFinished(mAnimation.getAnimationType());
567 processPendingInfoBars();
571 * Searches a given view's child views for an instance of {@link InfoBarContainer}.
573 * @param parentView View to be searched for
574 * @return {@link InfoBarContainer} instance if it's one of the child views;
575 * otherwise {@code null}.
577 public static InfoBarContainer childViewOf(ViewGroup parentView) {
578 for (int i = 0; i < parentView.getChildCount(); i++) {
579 if (parentView.getChildAt(i) instanceof InfoBarContainer) {
580 return (InfoBarContainer) parentView.getChildAt(i);
586 public long getNative() {
587 return mNativeInfoBarContainer;
590 private native long nativeInit(WebContents webContents, AutoLoginDelegate autoLoginDelegate);
591 private native void nativeDestroy(long nativeInfoBarContainerAndroid);