InfoBarContainer.java revision 116680a4aac90f2aa7413d9095a592090648e557
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
5package org.chromium.chrome.browser.infobar;
6
7import android.animation.ObjectAnimator;
8import android.app.Activity;
9import android.graphics.Canvas;
10import android.view.Gravity;
11import android.view.MotionEvent;
12import android.view.View;
13import android.view.ViewGroup;
14import android.widget.FrameLayout;
15import android.widget.LinearLayout;
16
17import com.google.common.annotations.VisibleForTesting;
18
19import org.chromium.base.CalledByNative;
20import org.chromium.content_public.browser.WebContents;
21import org.chromium.ui.UiUtils;
22
23import java.util.ArrayDeque;
24import java.util.ArrayList;
25import java.util.Iterator;
26import java.util.LinkedList;
27
28
29/**
30 * A container for all the infobars of a specific tab.
31 * Note that infobars creation can be initiated from Java of from native code.
32 * When initiated from native code, special code is needed to keep the Java and native infobar in
33 * sync, see NativeInfoBar.
34 */
35public class InfoBarContainer extends LinearLayout {
36    private static final String TAG = "InfoBarContainer";
37    private static final long REATTACH_FADE_IN_MS = 250;
38
39    /**
40     * A listener for the InfoBar animation.
41     */
42    public interface InfoBarAnimationListener {
43        /**
44         * Notifies the subscriber when an animation is completed.
45         */
46        void notifyAnimationFinished(int animationType);
47    }
48
49    private static class InfoBarTransitionInfo {
50        // InfoBar being animated.
51        public InfoBar target;
52
53        // View to replace the current View shown by the ContentWrapperView.
54        public View toShow;
55
56        // Which type of animation needs to be performed.
57        public int animationType;
58
59        public InfoBarTransitionInfo(InfoBar bar, View view, int type) {
60            assert type >= AnimationHelper.ANIMATION_TYPE_SHOW;
61            assert type < AnimationHelper.ANIMATION_TYPE_BOUNDARY;
62
63            target = bar;
64            toShow = view;
65            animationType = type;
66        }
67    }
68
69    private InfoBarAnimationListener mAnimationListener;
70
71    // Native InfoBarContainer pointer which will be set by nativeInit()
72    private long mNativeInfoBarContainer;
73
74    private final Activity mActivity;
75
76    private final AutoLoginDelegate mAutoLoginDelegate;
77
78    // The list of all infobars in this container, regardless of whether they've been shown yet.
79    private final ArrayList<InfoBar> mInfoBars = new ArrayList<InfoBar>();
80
81    // We only animate changing infobars one at a time.
82    private final ArrayDeque<InfoBarTransitionInfo> mInfoBarTransitions;
83
84    // Animation currently moving InfoBars around.
85    private AnimationHelper mAnimation;
86    private final FrameLayout mAnimationSizer;
87
88    // True when this container has been emptied and its native counterpart has been destroyed.
89    private boolean mDestroyed = false;
90
91    // The id of the tab associated with us. Set to Tab.INVALID_TAB_ID if no tab is associated.
92    private int mTabId;
93
94    // Parent view that contains us.
95    private ViewGroup mParentView;
96
97    public InfoBarContainer(Activity activity, AutoLoginProcessor autoLoginProcessor,
98            int tabId, ViewGroup parentView, WebContents webContents) {
99        super(activity);
100        setOrientation(LinearLayout.VERTICAL);
101        mAnimationListener = null;
102        mInfoBarTransitions = new ArrayDeque<InfoBarTransitionInfo>();
103
104        mAutoLoginDelegate = new AutoLoginDelegate(autoLoginProcessor, activity);
105        mActivity = activity;
106        mTabId = tabId;
107        mParentView = parentView;
108
109        mAnimationSizer = new FrameLayout(activity);
110        mAnimationSizer.setVisibility(INVISIBLE);
111
112        setGravity(Gravity.BOTTOM);
113
114        // Chromium's InfoBarContainer may add an InfoBar immediately during this initialization
115        // call, so make sure everything in the InfoBarContainer is completely ready beforehand.
116        mNativeInfoBarContainer = nativeInit(webContents, mAutoLoginDelegate);
117    }
118
119    public void setAnimationListener(InfoBarAnimationListener listener) {
120        mAnimationListener = listener;
121    }
122
123    @VisibleForTesting
124    public InfoBarAnimationListener getAnimationListener() {
125        return mAnimationListener;
126    }
127
128    public boolean areInfoBarsOnTop() {
129        return false;
130    }
131
132    @Override
133    public boolean onInterceptTouchEvent(MotionEvent ev) {
134        // Trap any attempts to fiddle with the Views while we're animating.
135        return mAnimation != null;
136    }
137
138    @Override
139    public boolean onTouchEvent(MotionEvent event) {
140        // Consume all motion events so they do not reach the ContentView.
141        return true;
142    }
143
144    private void addToParentView() {
145        if (mParentView != null && mParentView.indexOfChild(this) == -1) {
146            mParentView.addView(this, createLayoutParams());
147        }
148    }
149
150    private FrameLayout.LayoutParams createLayoutParams() {
151        return new FrameLayout.LayoutParams(
152                LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM);
153    }
154
155    public void removeFromParentView() {
156        if (getParent() != null) {
157            ((ViewGroup) getParent()).removeView(this);
158        }
159    }
160
161    /**
162     * Called when the parent {@link android.view.ViewGroup} has changed for
163     * this container.
164     */
165    public void onParentViewChanged(int tabId, ViewGroup parentView) {
166        mTabId = tabId;
167        mParentView = parentView;
168
169        removeFromParentView();
170        addToParentView();
171    }
172
173    @Override
174    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
175        if (mAnimation == null || child != mAnimation.getTarget()) {
176            return super.drawChild(canvas, child, drawingTime);
177        }
178        // When infobars are on top, the new infobar Z-order is greater than the previous infobar,
179        // which means it shows on top during the animation. We cannot change the Z-order in the
180        // linear layout, it is driven by the insertion index.
181        // So we simply clip the children to their bounds to make sure the new infobar does not
182        // paint over.
183        boolean retVal;
184        canvas.save();
185        canvas.clipRect(mAnimation.getTarget().getClippingRect());
186        retVal = super.drawChild(canvas, child, drawingTime);
187        canvas.restore();
188        return retVal;
189    }
190
191    @Override
192    protected void onAttachedToWindow() {
193        super.onAttachedToWindow();
194        ObjectAnimator.ofFloat(this, "alpha", 0.f, 1.f).setDuration(REATTACH_FADE_IN_MS).start();
195        setVisibility(VISIBLE);
196    }
197
198    @Override
199    protected void onDetachedFromWindow() {
200        super.onDetachedFromWindow();
201        setVisibility(INVISIBLE);
202    }
203
204    /**
205     * Adds an InfoBar to the view hierarchy.
206     * @param infoBar InfoBar to add to the View hierarchy.
207     */
208    @CalledByNative
209    public void addInfoBar(InfoBar infoBar) {
210        assert !mDestroyed;
211        if (infoBar == null) {
212            return;
213        }
214        if (mInfoBars.contains(infoBar)) {
215            assert false : "Trying to add an info bar that has already been added.";
216            return;
217        }
218
219        // We add the infobar immediately to mInfoBars but we wait for the animation to end to
220        // notify it's been added, as tests rely on this notification but expects the infobar view
221        // to be available when they get the notification.
222        mInfoBars.add(infoBar);
223        infoBar.setContext(mActivity);
224        infoBar.setInfoBarContainer(this);
225
226        enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_SHOW);
227    }
228
229    /**
230     * Returns the latest InfoBarTransitionInfo that deals with the given InfoBar.
231     * @param toFind InfoBar that we're looking for.
232     */
233    public InfoBarTransitionInfo findLastTransitionForInfoBar(InfoBar toFind) {
234        Iterator<InfoBarTransitionInfo> iterator = mInfoBarTransitions.descendingIterator();
235        while (iterator.hasNext()) {
236            InfoBarTransitionInfo info = iterator.next();
237            if (info.target == toFind) return info;
238        }
239        return null;
240    }
241
242    /**
243     * Animates swapping out the current View in the {@code infoBar} with {@code toShow} without
244     * destroying or dismissing the entire InfoBar.
245     * @param infoBar InfoBar that is having its content replaced.
246     * @param toShow View representing the InfoBar's new contents.
247     */
248    public void swapInfoBarViews(InfoBar infoBar, View toShow) {
249        assert !mDestroyed;
250
251        if (!mInfoBars.contains(infoBar)) {
252            assert false : "Trying to swap an InfoBar that is not in this container.";
253            return;
254        }
255
256        InfoBarTransitionInfo transition = findLastTransitionForInfoBar(infoBar);
257        if (transition != null && transition.toShow == toShow) {
258            assert false : "Tried to enqueue the same swap twice in a row.";
259            return;
260        }
261
262        enqueueInfoBarAnimation(infoBar, toShow, AnimationHelper.ANIMATION_TYPE_SWAP);
263    }
264
265    /**
266     * Removes an InfoBar from the view hierarchy.
267     * @param infoBar InfoBar to remove from the View hierarchy.
268     */
269    public void removeInfoBar(InfoBar infoBar) {
270        assert !mDestroyed;
271
272        if (!mInfoBars.remove(infoBar)) {
273            assert false : "Trying to remove an InfoBar that is not in this container.";
274            return;
275        }
276
277        // If an InfoBar is told to hide itself before it has a chance to be shown, don't bother
278        // with animating any of it.
279        boolean collapseAnimations = false;
280        ArrayDeque<InfoBarTransitionInfo> transitionCopy =
281                new ArrayDeque<InfoBarTransitionInfo>(mInfoBarTransitions);
282        for (InfoBarTransitionInfo info : transitionCopy) {
283            if (info.target == infoBar) {
284                if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
285                    // We can assert that two attempts to show the same InfoBar won't be in the
286                    // deque simultaneously because of the check in addInfoBar().
287                    assert !collapseAnimations;
288                    collapseAnimations = true;
289                }
290                if (collapseAnimations) {
291                    mInfoBarTransitions.remove(info);
292                }
293            }
294        }
295
296        if (!collapseAnimations) {
297            enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_HIDE);
298        }
299    }
300
301    /**
302     * Enqueue a new animation to run and kicks off the animation sequence.
303     */
304    private void enqueueInfoBarAnimation(InfoBar infoBar, View toShow, int animationType) {
305        InfoBarTransitionInfo info = new InfoBarTransitionInfo(infoBar, toShow, animationType);
306        mInfoBarTransitions.add(info);
307        processPendingInfoBars();
308    }
309
310    @Override
311    protected void onLayout(boolean changed, int l, int t, int r, int b) {
312        // Hide the infobars when the keyboard is showing.
313        boolean isShowing = (getVisibility() == View.VISIBLE);
314        if (UiUtils.isKeyboardShowing(mActivity, this)) {
315            if (isShowing) {
316                setVisibility(View.INVISIBLE);
317            }
318        } else {
319            if (!isShowing) {
320                setVisibility(View.VISIBLE);
321            }
322        }
323        super.onLayout(changed, l, t, r, b);
324    }
325
326    /**
327     * @return True when this container has been emptied and its native counterpart has been
328     *         destroyed.
329     */
330    public boolean hasBeenDestroyed() {
331        return mDestroyed;
332    }
333
334    private void processPendingInfoBars() {
335        if (mAnimation != null || mInfoBarTransitions.isEmpty()) return;
336
337        // Start animating what has to be animated.
338        InfoBarTransitionInfo info = mInfoBarTransitions.remove();
339        View toShow = info.toShow;
340        ContentWrapperView targetView;
341
342        addToParentView();
343
344        if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
345            targetView = info.target.getContentWrapper(true);
346            assert mInfoBars.contains(info.target);
347            toShow = targetView.detachCurrentView();
348            addView(targetView, 0,
349                    new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
350        } else {
351            targetView = info.target.getContentWrapper(false);
352        }
353
354        // Kick off the animation.
355        mAnimation = new AnimationHelper(this, targetView, info.target, toShow, info.animationType);
356        mAnimation.start();
357    }
358
359    // Called by the tab when it has started loading a new page.
360    public void onPageStarted() {
361        LinkedList<InfoBar> barsToRemove = new LinkedList<InfoBar>();
362
363        for (InfoBar infoBar : mInfoBars) {
364            if (infoBar.shouldExpire()) {
365                barsToRemove.add(infoBar);
366            }
367        }
368
369        for (InfoBar infoBar : barsToRemove) {
370            infoBar.dismissJavaOnlyInfoBar();
371        }
372    }
373
374    /**
375     * Returns the id of the tab we are associated with.
376     */
377    public int getTabId() {
378        return mTabId;
379    }
380
381    public void destroy() {
382        mDestroyed = true;
383        removeAllViews();
384        if (mNativeInfoBarContainer != 0) {
385            nativeDestroy(mNativeInfoBarContainer);
386        }
387        mInfoBarTransitions.clear();
388    }
389
390    /**
391     * @return all of the InfoBars held in this container.
392     */
393    @VisibleForTesting
394    public ArrayList<InfoBar> getInfoBars() {
395        return mInfoBars;
396    }
397
398    /**
399     * Dismisses all {@link AutoLoginInfoBar}s in this {@link InfoBarContainer} that are for
400     * {@code accountName} and {@code authToken}.  This also resets all {@link InfoBar}s that are
401     * for a different request.
402     * @param accountName The name of the account request is being accessed for.
403     * @param authToken The authentication token access is being requested for.
404     * @param success Whether or not the authentication attempt was successful.
405     * @param result The resulting token for the auto login request (ignored if {@code success} is
406     *               {@code false}.
407     */
408    public void processAutoLogin(String accountName, String authToken, boolean success,
409            String result) {
410        mAutoLoginDelegate.dismissAutoLogins(accountName, authToken, success, result);
411    }
412
413    /**
414     * Dismiss all auto logins infobars without processing any result.
415     */
416    public void dismissAutoLoginInfoBars() {
417        mAutoLoginDelegate.dismissAutoLogins("", "", false, "");
418    }
419
420    public void prepareTransition(View toShow) {
421        if (toShow != null) {
422            // In order to animate the addition of the infobar, we need a layout first.
423            // Attach the child to invisible layout so that we can get measurements for it without
424            // moving everything in the real container.
425            ViewGroup parent = (ViewGroup) toShow.getParent();
426            if (parent != null) parent.removeView(toShow);
427
428            assert mAnimationSizer.getParent() == null;
429            mParentView.addView(mAnimationSizer, createLayoutParams());
430            mAnimationSizer.addView(toShow, 0,
431                    new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
432            mAnimationSizer.requestLayout();
433        }
434    }
435
436    /**
437     * Finishes off whatever animation is running.
438     */
439    public void finishTransition() {
440        assert mAnimation != null;
441
442        // If the InfoBar was hidden, get rid of its View entirely.
443        if (mAnimation.getAnimationType() == AnimationHelper.ANIMATION_TYPE_HIDE) {
444            removeView(mAnimation.getTarget());
445        }
446
447        // Reset all translations and put everything where they need to be.
448        for (int i = 0; i < getChildCount(); ++i) {
449            View view = getChildAt(i);
450            view.setTranslationY(0);
451        }
452        requestLayout();
453
454        // If there are no infobars shown, there is no need to keep the infobar container in the
455        // view hierarchy.
456        if (getChildCount() == 0) {
457            removeFromParentView();
458        }
459
460        if (mAnimationSizer.getParent() != null) {
461            ((ViewGroup) mAnimationSizer.getParent()).removeView(mAnimationSizer);
462        }
463
464        // Notify interested parties and move on to the next animation.
465        if (mAnimationListener != null) {
466            mAnimationListener.notifyAnimationFinished(mAnimation.getAnimationType());
467        }
468        mAnimation = null;
469        processPendingInfoBars();
470    }
471
472    /**
473     * Searches a given view's child views for an instance of {@link InfoBarContainer}.
474     *
475     * @param parentView View to be searched for
476     * @return {@link InfoBarContainer} instance if it's one of the child views;
477     *     otherwise {@code null}.
478     */
479    public static InfoBarContainer childViewOf(ViewGroup parentView) {
480        for (int i = 0; i < parentView.getChildCount(); i++) {
481            if (parentView.getChildAt(i) instanceof InfoBarContainer) {
482                return (InfoBarContainer) parentView.getChildAt(i);
483            }
484        }
485        return null;
486    }
487
488    public long getNative() {
489        return mNativeInfoBarContainer;
490    }
491
492    private native long nativeInit(WebContents webContents, AutoLoginDelegate autoLoginDelegate);
493
494    private native void nativeDestroy(long nativeInfoBarContainerAndroid);
495}
496