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 com.google.common.annotations.VisibleForTesting;
21 import org.chromium.base.CalledByNative;
22 import org.chromium.chrome.R;
23 import org.chromium.content_public.browser.WebContents;
24 import org.chromium.ui.UiUtils;
25 import org.chromium.ui.base.DeviceFormFactor;
27 import java.util.ArrayDeque;
28 import java.util.ArrayList;
29 import java.util.Iterator;
30 import java.util.LinkedList;
34 * A container for all the infobars of a specific tab.
35 * Note that infobars creation can be initiated from Java of from native code.
36 * When initiated from native code, special code is needed to keep the Java and native infobar in
37 * sync, see NativeInfoBar.
39 public class InfoBarContainer extends ScrollView {
40 private static final String TAG = "InfoBarContainer";
41 private static final long REATTACH_FADE_IN_MS = 250;
42 private static final int TAB_STRIP_AND_TOOLBAR_HEIGHT_PHONE_DP = 56;
43 private static final int TAB_STRIP_AND_TOOLBAR_HEIGHT_TABLET_DP = 96;
46 * A listener for the InfoBar animation.
48 public interface InfoBarAnimationListener {
50 * Notifies the subscriber when an animation is completed.
52 void notifyAnimationFinished(int animationType);
55 private static class InfoBarTransitionInfo {
56 // InfoBar being animated.
57 public InfoBar target;
59 // View to replace the current View shown by the ContentWrapperView.
62 // Which type of animation needs to be performed.
63 public int animationType;
65 public InfoBarTransitionInfo(InfoBar bar, View view, int type) {
66 assert type >= AnimationHelper.ANIMATION_TYPE_SHOW;
67 assert type < AnimationHelper.ANIMATION_TYPE_BOUNDARY;
75 private InfoBarAnimationListener mAnimationListener;
77 // Native InfoBarContainer pointer which will be set by nativeInit()
78 private long mNativeInfoBarContainer;
80 private final Activity mActivity;
82 private final AutoLoginDelegate mAutoLoginDelegate;
84 // The list of all infobars in this container, regardless of whether they've been shown yet.
85 private final ArrayList<InfoBar> mInfoBars = new ArrayList<InfoBar>();
87 // We only animate changing infobars one at a time.
88 private final ArrayDeque<InfoBarTransitionInfo> mInfoBarTransitions;
90 // Animation currently moving InfoBars around.
91 private AnimationHelper mAnimation;
92 private final FrameLayout mAnimationSizer;
94 // True when this container has been emptied and its native counterpart has been destroyed.
95 private boolean mDestroyed = false;
97 // The id of the tab associated with us. Set to Tab.INVALID_TAB_ID if no tab is associated.
100 // Parent view that contains us.
101 private ViewGroup mParentView;
103 // The LinearLayout that holds the infobars. This is the only child of the InfoBarContainer.
104 private LinearLayout mLinearLayout;
106 // These values are used in onLayout() to keep the infobars fixed to the bottom of the screen
107 // when infobars are added or removed.
109 private int mInnerHeight;
110 private int mDistanceFromBottom;
112 private Paint mTopBorderPaint;
114 public InfoBarContainer(Activity activity, AutoLoginProcessor autoLoginProcessor,
115 int tabId, ViewGroup parentView, WebContents webContents) {
118 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
119 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM);
120 int topMarginDp = DeviceFormFactor.isTablet(activity)
121 ? TAB_STRIP_AND_TOOLBAR_HEIGHT_TABLET_DP
122 : TAB_STRIP_AND_TOOLBAR_HEIGHT_PHONE_DP;
123 lp.topMargin = Math.round(topMarginDp * getResources().getDisplayMetrics().density);
126 mLinearLayout = new LinearLayout(activity);
127 mLinearLayout.setOrientation(LinearLayout.VERTICAL);
128 addView(mLinearLayout,
129 new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
131 mAnimationListener = null;
132 mInfoBarTransitions = new ArrayDeque<InfoBarTransitionInfo>();
134 mAutoLoginDelegate = new AutoLoginDelegate(autoLoginProcessor, activity);
135 mActivity = activity;
137 mParentView = parentView;
139 mAnimationSizer = new FrameLayout(activity);
140 mAnimationSizer.setVisibility(INVISIBLE);
142 // Chromium's InfoBarContainer may add an InfoBar immediately during this initialization
143 // call, so make sure everything in the InfoBarContainer is completely ready beforehand.
144 mNativeInfoBarContainer = nativeInit(webContents, mAutoLoginDelegate);
148 * @return The LinearLayout that holds the infobars (i.e. the ContentWrapperViews).
150 LinearLayout getLinearLayout() {
151 return mLinearLayout;
154 public void setAnimationListener(InfoBarAnimationListener listener) {
155 mAnimationListener = listener;
159 public InfoBarAnimationListener getAnimationListener() {
160 return mAnimationListener;
164 public boolean onInterceptTouchEvent(MotionEvent ev) {
165 // Trap any attempts to fiddle with the infobars while we're animating.
166 return super.onInterceptTouchEvent(ev) || mAnimation != null;
169 private void addToParentView() {
170 if (mParentView != null && mParentView.indexOfChild(this) == -1) {
171 mParentView.addView(this);
175 public void removeFromParentView() {
176 if (getParent() != null) {
177 ((ViewGroup) getParent()).removeView(this);
182 * Called when the parent {@link android.view.ViewGroup} has changed for
185 public void onParentViewChanged(int tabId, ViewGroup parentView) {
187 mParentView = parentView;
189 removeFromParentView();
194 protected void onAttachedToWindow() {
195 super.onAttachedToWindow();
196 ObjectAnimator.ofFloat(this, "alpha", 0.f, 1.f).setDuration(REATTACH_FADE_IN_MS).start();
197 setVisibility(VISIBLE);
201 protected void onDetachedFromWindow() {
202 super.onDetachedFromWindow();
203 setVisibility(INVISIBLE);
207 * Adds an InfoBar to the view hierarchy.
208 * @param infoBar InfoBar to add to the View hierarchy.
211 public void addInfoBar(InfoBar infoBar) {
213 if (infoBar == null) {
216 if (mInfoBars.contains(infoBar)) {
217 assert false : "Trying to add an info bar that has already been added.";
221 // We add the infobar immediately to mInfoBars but we wait for the animation to end to
222 // notify it's been added, as tests rely on this notification but expects the infobar view
223 // to be available when they get the notification.
224 mInfoBars.add(infoBar);
225 infoBar.setContext(mActivity);
226 infoBar.setInfoBarContainer(this);
228 enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_SHOW);
232 * Returns the latest InfoBarTransitionInfo that deals with the given InfoBar.
233 * @param toFind InfoBar that we're looking for.
235 public InfoBarTransitionInfo findLastTransitionForInfoBar(InfoBar toFind) {
236 Iterator<InfoBarTransitionInfo> iterator = mInfoBarTransitions.descendingIterator();
237 while (iterator.hasNext()) {
238 InfoBarTransitionInfo info = iterator.next();
239 if (info.target == toFind) return info;
245 * Animates swapping out the current View in the {@code infoBar} with {@code toShow} without
246 * destroying or dismissing the entire InfoBar.
247 * @param infoBar InfoBar that is having its content replaced.
248 * @param toShow View representing the InfoBar's new contents.
250 public void swapInfoBarViews(InfoBar infoBar, View toShow) {
253 if (!mInfoBars.contains(infoBar)) {
254 assert false : "Trying to swap an InfoBar that is not in this container.";
258 InfoBarTransitionInfo transition = findLastTransitionForInfoBar(infoBar);
259 if (transition != null && transition.toShow == toShow) {
260 assert false : "Tried to enqueue the same swap twice in a row.";
264 enqueueInfoBarAnimation(infoBar, toShow, AnimationHelper.ANIMATION_TYPE_SWAP);
268 * Removes an InfoBar from the view hierarchy.
269 * @param infoBar InfoBar to remove from the View hierarchy.
271 public void removeInfoBar(InfoBar infoBar) {
274 if (!mInfoBars.remove(infoBar)) {
275 assert false : "Trying to remove an InfoBar that is not in this container.";
279 // If an InfoBar is told to hide itself before it has a chance to be shown, don't bother
280 // with animating any of it.
281 boolean collapseAnimations = false;
282 ArrayDeque<InfoBarTransitionInfo> transitionCopy =
283 new ArrayDeque<InfoBarTransitionInfo>(mInfoBarTransitions);
284 for (InfoBarTransitionInfo info : transitionCopy) {
285 if (info.target == infoBar) {
286 if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
287 // We can assert that two attempts to show the same InfoBar won't be in the
288 // deque simultaneously because of the check in addInfoBar().
289 assert !collapseAnimations;
290 collapseAnimations = true;
292 if (collapseAnimations) {
293 mInfoBarTransitions.remove(info);
298 if (!collapseAnimations) {
299 enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_HIDE);
304 * Enqueue a new animation to run and kicks off the animation sequence.
306 private void enqueueInfoBarAnimation(InfoBar infoBar, View toShow, int animationType) {
307 InfoBarTransitionInfo info = new InfoBarTransitionInfo(infoBar, toShow, animationType);
308 mInfoBarTransitions.add(info);
309 processPendingInfoBars();
313 protected void onLayout(boolean changed, int l, int t, int r, int b) {
314 // Hide the infobars when the keyboard is showing.
315 boolean isShowing = (getVisibility() == View.VISIBLE);
316 if (UiUtils.isKeyboardShowing(mActivity, this)) {
318 setVisibility(View.INVISIBLE);
322 setVisibility(View.VISIBLE);
325 super.onLayout(changed, l, t, r, b);
327 // Keep the infobars fixed to the bottom of the screen when infobars are added or removed.
328 // Otherwise, infobars jump around when appearing or disappearing on small devices.
329 int newHeight = getHeight();
330 int newInnerHeight = mLinearLayout.getHeight();
331 if (mInnerHeight != newInnerHeight) {
332 int newScrollY = newInnerHeight - newHeight - mDistanceFromBottom;
333 scrollTo(0, newScrollY);
336 mInnerHeight = newInnerHeight;
337 mDistanceFromBottom = mInnerHeight - mHeight - getScrollY();
341 protected void onScrollChanged(int l, int t, int oldl, int oldt) {
342 super.onScrollChanged(l, t, oldl, oldt);
343 mDistanceFromBottom = mInnerHeight - mHeight - getScrollY();
347 protected void dispatchDraw(Canvas canvas) {
348 super.dispatchDraw(canvas);
350 // If the infobars overflow the ScrollView, draw a border at the top of the ScrollView.
351 // This prevents the topmost infobar from blending into the page when fullscreen mode
353 if (getScrollY() != 0) {
354 if (mTopBorderPaint == null) {
355 mTopBorderPaint = new Paint();
356 mTopBorderPaint.setColor(
357 getResources().getColor(R.color.infobar_background_separator));
359 int height = ContentWrapperView.getBoundaryHeight(getContext());
360 canvas.drawRect(0, getScrollY(), getWidth(), getScrollY() + height, mTopBorderPaint);
365 * @return True when this container has been emptied and its native counterpart has been
368 public boolean hasBeenDestroyed() {
372 private void processPendingInfoBars() {
373 if (mAnimation != null || mInfoBarTransitions.isEmpty()) return;
375 // Start animating what has to be animated.
376 InfoBarTransitionInfo info = mInfoBarTransitions.remove();
377 View toShow = info.toShow;
378 ContentWrapperView targetView;
382 if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
383 targetView = info.target.getContentWrapper(true);
384 assert mInfoBars.contains(info.target);
385 toShow = targetView.detachCurrentView();
386 mLinearLayout.addView(targetView, 0,
387 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
389 targetView = info.target.getContentWrapper(false);
392 // Kick off the animation.
393 mAnimation = new AnimationHelper(this, targetView, info.target, toShow, info.animationType);
397 // Called by the tab when it has started loading a new page.
398 public void onPageStarted() {
399 LinkedList<InfoBar> barsToRemove = new LinkedList<InfoBar>();
401 for (InfoBar infoBar : mInfoBars) {
402 if (infoBar.shouldExpire()) {
403 barsToRemove.add(infoBar);
407 for (InfoBar infoBar : barsToRemove) {
408 infoBar.dismissJavaOnlyInfoBar();
413 * Returns the id of the tab we are associated with.
415 public int getTabId() {
419 public void destroy() {
421 mLinearLayout.removeAllViews();
422 if (mNativeInfoBarContainer != 0) {
423 nativeDestroy(mNativeInfoBarContainer);
425 mInfoBarTransitions.clear();
429 * @return all of the InfoBars held in this container.
432 public ArrayList<InfoBar> getInfoBars() {
437 * Dismisses all {@link AutoLoginInfoBar}s in this {@link InfoBarContainer} that are for
438 * {@code accountName} and {@code authToken}. This also resets all {@link InfoBar}s that are
439 * for a different request.
440 * @param accountName The name of the account request is being accessed for.
441 * @param authToken The authentication token access is being requested for.
442 * @param success Whether or not the authentication attempt was successful.
443 * @param result The resulting token for the auto login request (ignored if {@code success} is
446 public void processAutoLogin(String accountName, String authToken, boolean success,
448 mAutoLoginDelegate.dismissAutoLogins(accountName, authToken, success, result);
452 * Dismiss all auto logins infobars without processing any result.
454 public void dismissAutoLoginInfoBars() {
455 mAutoLoginDelegate.dismissAutoLogins("", "", false, "");
458 public void prepareTransition(View toShow) {
459 if (toShow != null) {
460 // In order to animate the addition of the infobar, we need a layout first.
461 // Attach the child to invisible layout so that we can get measurements for it without
462 // moving everything in the real container.
463 ViewGroup parent = (ViewGroup) toShow.getParent();
464 if (parent != null) parent.removeView(toShow);
466 assert mAnimationSizer.getParent() == null;
467 mParentView.addView(mAnimationSizer, new FrameLayout.LayoutParams(
468 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
469 mAnimationSizer.addView(toShow, 0,
470 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
471 mAnimationSizer.requestLayout();
476 * Finishes off whatever animation is running.
478 public void finishTransition() {
479 assert mAnimation != null;
481 // If the InfoBar was hidden, get rid of its View entirely.
482 if (mAnimation.getAnimationType() == AnimationHelper.ANIMATION_TYPE_HIDE) {
483 mLinearLayout.removeView(mAnimation.getTarget());
486 // Reset all translations and put everything where they need to be.
487 for (int i = 0; i < mLinearLayout.getChildCount(); ++i) {
488 View view = mLinearLayout.getChildAt(i);
489 view.setTranslationY(0);
493 // If there are no infobars shown, there is no need to keep the infobar container in the
495 if (mLinearLayout.getChildCount() == 0) {
496 removeFromParentView();
499 if (mAnimationSizer.getParent() != null) {
500 ((ViewGroup) mAnimationSizer.getParent()).removeView(mAnimationSizer);
503 // Notify interested parties and move on to the next animation.
504 if (mAnimationListener != null) {
505 mAnimationListener.notifyAnimationFinished(mAnimation.getAnimationType());
508 processPendingInfoBars();
512 * Searches a given view's child views for an instance of {@link InfoBarContainer}.
514 * @param parentView View to be searched for
515 * @return {@link InfoBarContainer} instance if it's one of the child views;
516 * otherwise {@code null}.
518 public static InfoBarContainer childViewOf(ViewGroup parentView) {
519 for (int i = 0; i < parentView.getChildCount(); i++) {
520 if (parentView.getChildAt(i) instanceof InfoBarContainer) {
521 return (InfoBarContainer) parentView.getChildAt(i);
527 public long getNative() {
528 return mNativeInfoBarContainer;
531 private native long nativeInit(WebContents webContents, AutoLoginDelegate autoLoginDelegate);
532 private native void nativeDestroy(long nativeInfoBarContainerAndroid);