Home | History | Annotate | Download | only in infobar
      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.
      4 
      5 package org.chromium.chrome.browser.infobar;
      6 
      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;
     18 
     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;
     28 
     29 import java.util.ArrayDeque;
     30 import java.util.ArrayList;
     31 import java.util.Iterator;
     32 import java.util.LinkedList;
     33 
     34 
     35 /**
     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.
     40  */
     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;
     46 
     47     /**
     48      * A listener for the InfoBar animation.
     49      */
     50     public interface InfoBarAnimationListener {
     51         /**
     52          * Notifies the subscriber when an animation is completed.
     53          */
     54         void notifyAnimationFinished(int animationType);
     55     }
     56 
     57     private static class InfoBarTransitionInfo {
     58         // InfoBar being animated.
     59         public InfoBar target;
     60 
     61         // View to replace the current View shown by the ContentWrapperView.
     62         public View toShow;
     63 
     64         // Which type of animation needs to be performed.
     65         public int animationType;
     66 
     67         public InfoBarTransitionInfo(InfoBar bar, View view, int type) {
     68             assert type >= AnimationHelper.ANIMATION_TYPE_SHOW;
     69             assert type < AnimationHelper.ANIMATION_TYPE_BOUNDARY;
     70 
     71             target = bar;
     72             toShow = view;
     73             animationType = type;
     74         }
     75     }
     76 
     77     private InfoBarAnimationListener mAnimationListener;
     78 
     79     // Native InfoBarContainer pointer which will be set by nativeInit()
     80     private long mNativeInfoBarContainer;
     81 
     82     private final Activity mActivity;
     83 
     84     private final AutoLoginDelegate mAutoLoginDelegate;
     85 
     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>();
     88 
     89     // We only animate changing infobars one at a time.
     90     private final ArrayDeque<InfoBarTransitionInfo> mInfoBarTransitions;
     91 
     92     // Animation currently moving InfoBars around.
     93     private AnimationHelper mAnimation;
     94     private final FrameLayout mAnimationSizer;
     95 
     96     // True when this container has been emptied and its native counterpart has been destroyed.
     97     private boolean mDestroyed = false;
     98 
     99     // The id of the tab associated with us. Set to Tab.INVALID_TAB_ID if no tab is associated.
    100     private int mTabId;
    101 
    102     // Parent view that contains us.
    103     private ViewGroup mParentView;
    104 
    105     // The LinearLayout that holds the infobars. This is the only child of the InfoBarContainer.
    106     private LinearLayout mLinearLayout;
    107 
    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.
    110     private int mHeight;
    111     private int mInnerHeight;
    112     private int mDistanceFromBottom;
    113 
    114     private Paint mTopBorderPaint;
    115 
    116     // Keeps the infobars from becoming visible when they normally would.
    117     private boolean mDoStayInvisible;
    118     private TabObserver mTabObserver;
    119 
    120     public InfoBarContainer(Activity activity, AutoLoginProcessor autoLoginProcessor,
    121             int tabId, ViewGroup parentView, WebContents webContents) {
    122         super(activity);
    123 
    124         // Workaround for http://crbug.com/407149. See explanation in onMeasure() below.
    125         setVerticalScrollBarEnabled(false);
    126 
    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);
    133         setLayoutParams(lp);
    134 
    135         mLinearLayout = new LinearLayout(activity);
    136         mLinearLayout.setOrientation(LinearLayout.VERTICAL);
    137         addView(mLinearLayout,
    138                 new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
    139 
    140         mAnimationListener = null;
    141         mInfoBarTransitions = new ArrayDeque<InfoBarTransitionInfo>();
    142 
    143         mAutoLoginDelegate = new AutoLoginDelegate(autoLoginProcessor, activity);
    144         mActivity = activity;
    145         mTabId = tabId;
    146         mParentView = parentView;
    147 
    148         mAnimationSizer = new FrameLayout(activity);
    149         mAnimationSizer.setVisibility(INVISIBLE);
    150 
    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);
    154     }
    155 
    156     @Override
    157     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    158         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    159 
    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);
    166         }
    167     }
    168 
    169     /**
    170      * @return The LinearLayout that holds the infobars (i.e. the ContentWrapperViews).
    171      */
    172     LinearLayout getLinearLayout() {
    173         return mLinearLayout;
    174     }
    175 
    176     @VisibleForTesting
    177     public void setAnimationListener(InfoBarAnimationListener listener) {
    178         mAnimationListener = listener;
    179     }
    180 
    181     @VisibleForTesting
    182     public InfoBarAnimationListener getAnimationListener() {
    183         return mAnimationListener;
    184     }
    185 
    186     @Override
    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;
    190     }
    191 
    192     private void addToParentView() {
    193         if (mParentView != null && mParentView.indexOfChild(this) == -1) {
    194             mParentView.addView(this);
    195         }
    196     }
    197 
    198     public void removeFromParentView() {
    199         if (getParent() != null) {
    200             ((ViewGroup) getParent()).removeView(this);
    201         }
    202     }
    203 
    204     /**
    205      * Called when the parent {@link android.view.ViewGroup} has changed for
    206      * this container.
    207      */
    208     public void onParentViewChanged(int tabId, ViewGroup parentView) {
    209         mTabId = tabId;
    210         mParentView = parentView;
    211 
    212         removeFromParentView();
    213         addToParentView();
    214     }
    215 
    216     /**
    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.
    222      */
    223     public void setDoStayInvisible(boolean doStayInvisible, Tab tab) {
    224         mDoStayInvisible = doStayInvisible;
    225         if (mTabObserver == null) mTabObserver = createTabObserver();
    226         if (doStayInvisible) {
    227             tab.addObserver(mTabObserver);
    228         } else {
    229             tab.removeObserver(mTabObserver);
    230         }
    231     }
    232 
    233     /**
    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.
    237      */
    238     private TabObserver createTabObserver() {
    239         return new EmptyTabObserver() {
    240             @Override
    241             public void onDidNavigateMainFrame(Tab tab, String url, String baseUrl,
    242                     boolean isNavigationToDifferentPage, boolean isFragmentNavigation,
    243                     int statusCode) {
    244                 setDoStayInvisible(false, tab);
    245             }
    246         };
    247     }
    248 
    249     @Override
    250     protected void onAttachedToWindow() {
    251         super.onAttachedToWindow();
    252         if (!mDoStayInvisible) {
    253             ObjectAnimator.ofFloat(this, "alpha", 0.f, 1.f).setDuration(REATTACH_FADE_IN_MS)
    254                     .start();
    255             setVisibility(VISIBLE);
    256         }
    257     }
    258 
    259     @Override
    260     protected void onDetachedFromWindow() {
    261         super.onDetachedFromWindow();
    262         setVisibility(INVISIBLE);
    263     }
    264 
    265     /**
    266      * Adds an InfoBar to the view hierarchy.
    267      * @param infoBar InfoBar to add to the View hierarchy.
    268      */
    269     @CalledByNative
    270     public void addInfoBar(InfoBar infoBar) {
    271         assert !mDestroyed;
    272         if (infoBar == null) {
    273             return;
    274         }
    275         if (mInfoBars.contains(infoBar)) {
    276             assert false : "Trying to add an info bar that has already been added.";
    277             return;
    278         }
    279 
    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);
    286 
    287         enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_SHOW);
    288     }
    289 
    290     /**
    291      * Returns the latest InfoBarTransitionInfo that deals with the given InfoBar.
    292      * @param toFind InfoBar that we're looking for.
    293      */
    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;
    299         }
    300         return null;
    301     }
    302 
    303     /**
    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.
    308      */
    309     public void swapInfoBarViews(InfoBar infoBar, View toShow) {
    310         assert !mDestroyed;
    311 
    312         if (!mInfoBars.contains(infoBar)) {
    313             assert false : "Trying to swap an InfoBar that is not in this container.";
    314             return;
    315         }
    316 
    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.";
    320             return;
    321         }
    322 
    323         enqueueInfoBarAnimation(infoBar, toShow, AnimationHelper.ANIMATION_TYPE_SWAP);
    324     }
    325 
    326     /**
    327      * Removes an InfoBar from the view hierarchy.
    328      * @param infoBar InfoBar to remove from the View hierarchy.
    329      */
    330     public void removeInfoBar(InfoBar infoBar) {
    331         assert !mDestroyed;
    332 
    333         if (!mInfoBars.remove(infoBar)) {
    334             assert false : "Trying to remove an InfoBar that is not in this container.";
    335             return;
    336         }
    337 
    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;
    350                 }
    351                 if (collapseAnimations) {
    352                     mInfoBarTransitions.remove(info);
    353                 }
    354             }
    355         }
    356 
    357         if (!collapseAnimations) {
    358             enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_HIDE);
    359         }
    360     }
    361 
    362     /**
    363      * Enqueue a new animation to run and kicks off the animation sequence.
    364      */
    365     private void enqueueInfoBarAnimation(InfoBar infoBar, View toShow, int animationType) {
    366         InfoBarTransitionInfo info = new InfoBarTransitionInfo(infoBar, toShow, animationType);
    367         mInfoBarTransitions.add(info);
    368         processPendingInfoBars();
    369     }
    370 
    371     @Override
    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)) {
    376             if (isShowing) {
    377                 setVisibility(View.INVISIBLE);
    378             }
    379         } else {
    380             if (!isShowing && !mDoStayInvisible) {
    381                 setVisibility(View.VISIBLE);
    382             }
    383         }
    384         super.onLayout(changed, l, t, r, b);
    385 
    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);
    393         }
    394         mHeight = newHeight;
    395         mInnerHeight = newInnerHeight;
    396         mDistanceFromBottom = mInnerHeight - mHeight - getScrollY();
    397     }
    398 
    399     @Override
    400     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    401         super.onScrollChanged(l, t, oldl, oldt);
    402         mDistanceFromBottom = mInnerHeight - mHeight - getScrollY();
    403     }
    404 
    405     @Override
    406     protected void dispatchDraw(Canvas canvas) {
    407         super.dispatchDraw(canvas);
    408 
    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
    411         // is active.
    412         if (getScrollY() != 0) {
    413             if (mTopBorderPaint == null) {
    414                 mTopBorderPaint = new Paint();
    415                 mTopBorderPaint.setColor(
    416                         getResources().getColor(R.color.infobar_background_separator));
    417             }
    418             int height = ContentWrapperView.getBoundaryHeight(getContext());
    419             canvas.drawRect(0, getScrollY(), getWidth(), getScrollY() + height, mTopBorderPaint);
    420         }
    421     }
    422 
    423     /**
    424      * @return True when this container has been emptied and its native counterpart has been
    425      *         destroyed.
    426      */
    427     public boolean hasBeenDestroyed() {
    428         return mDestroyed;
    429     }
    430 
    431     private void processPendingInfoBars() {
    432         if (mAnimation != null || mInfoBarTransitions.isEmpty()) return;
    433 
    434         // Start animating what has to be animated.
    435         InfoBarTransitionInfo info = mInfoBarTransitions.remove();
    436         View toShow = info.toShow;
    437         ContentWrapperView targetView;
    438 
    439         addToParentView();
    440 
    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));
    447         } else {
    448             targetView = info.target.getContentWrapper(false);
    449         }
    450 
    451         // Kick off the animation.
    452         mAnimation = new AnimationHelper(this, targetView, info.target, toShow, info.animationType);
    453         mAnimation.start();
    454     }
    455 
    456     // Called by the tab when it has started loading a new page.
    457     public void onPageStarted() {
    458         LinkedList<InfoBar> barsToRemove = new LinkedList<InfoBar>();
    459 
    460         for (InfoBar infoBar : mInfoBars) {
    461             if (infoBar.shouldExpire()) {
    462                 barsToRemove.add(infoBar);
    463             }
    464         }
    465 
    466         for (InfoBar infoBar : barsToRemove) {
    467             infoBar.dismissJavaOnlyInfoBar();
    468         }
    469     }
    470 
    471     /**
    472      * Returns the id of the tab we are associated with.
    473      */
    474     public int getTabId() {
    475         return mTabId;
    476     }
    477 
    478     public void destroy() {
    479         mDestroyed = true;
    480         mLinearLayout.removeAllViews();
    481         if (mNativeInfoBarContainer != 0) {
    482             nativeDestroy(mNativeInfoBarContainer);
    483         }
    484         mInfoBarTransitions.clear();
    485     }
    486 
    487     /**
    488      * @return all of the InfoBars held in this container.
    489      */
    490     @VisibleForTesting
    491     public ArrayList<InfoBar> getInfoBars() {
    492         return mInfoBars;
    493     }
    494 
    495     /**
    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
    503      *               {@code false}.
    504      */
    505     public void processAutoLogin(String accountName, String authToken, boolean success,
    506             String result) {
    507         mAutoLoginDelegate.dismissAutoLogins(accountName, authToken, success, result);
    508     }
    509 
    510     /**
    511      * Dismiss all auto logins infobars without processing any result.
    512      */
    513     public void dismissAutoLoginInfoBars() {
    514         mAutoLoginDelegate.dismissAutoLogins("", "", false, "");
    515     }
    516 
    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);
    524 
    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();
    531         }
    532     }
    533 
    534     /**
    535      * Finishes off whatever animation is running.
    536      */
    537     public void finishTransition() {
    538         assert mAnimation != null;
    539 
    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());
    543         }
    544 
    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);
    549         }
    550         requestLayout();
    551 
    552         // If there are no infobars shown, there is no need to keep the infobar container in the
    553         // view hierarchy.
    554         if (mLinearLayout.getChildCount() == 0) {
    555             removeFromParentView();
    556         }
    557 
    558         if (mAnimationSizer.getParent() != null) {
    559             ((ViewGroup) mAnimationSizer.getParent()).removeView(mAnimationSizer);
    560         }
    561 
    562         // Notify interested parties and move on to the next animation.
    563         if (mAnimationListener != null) {
    564             mAnimationListener.notifyAnimationFinished(mAnimation.getAnimationType());
    565         }
    566         mAnimation = null;
    567         processPendingInfoBars();
    568     }
    569 
    570     /**
    571      * Searches a given view's child views for an instance of {@link InfoBarContainer}.
    572      *
    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}.
    576      */
    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);
    581             }
    582         }
    583         return null;
    584     }
    585 
    586     public long getNative() {
    587         return mNativeInfoBarContainer;
    588     }
    589 
    590     private native long nativeInit(WebContents webContents, AutoLoginDelegate autoLoginDelegate);
    591     private native void nativeDestroy(long nativeInfoBarContainerAndroid);
    592 }
    593