- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / android / java / src / org / chromium / chrome / browser / infobar / InfoBarContainer.java
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.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;
16
17 import com.google.common.annotations.VisibleForTesting;
18
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;
23
24 import java.util.ArrayDeque;
25 import java.util.ArrayList;
26 import java.util.Iterator;
27 import java.util.LinkedList;
28
29
30 /**
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.
35  */
36 public class InfoBarContainer extends LinearLayout {
37     private static final String TAG = "InfoBarContainer";
38     private static final long REATTACH_FADE_IN_MS = 250;
39
40     public interface InfoBarAnimationListener {
41         /**
42          * Notifies the subscriber when an animation is completed.
43          */
44         void notifyAnimationFinished(int animationType);
45     }
46
47     private static class InfoBarTransitionInfo {
48         // InfoBar being animated.
49         public InfoBar target;
50
51         // View to replace the current View shown by the ContentWrapperView.
52         public View toShow;
53
54         // Which type of animation needs to be performed.
55         public int animationType;
56
57         public InfoBarTransitionInfo(InfoBar bar, View view, int type) {
58             assert type >= AnimationHelper.ANIMATION_TYPE_SHOW;
59             assert type < AnimationHelper.ANIMATION_TYPE_BOUNDARY;
60
61             target = bar;
62             toShow = view;
63             animationType = type;
64         }
65     }
66
67     private InfoBarAnimationListener mAnimationListener;
68
69     // Native InfoBarContainer pointer which will be set by nativeInit()
70     private int mNativeInfoBarContainer;
71
72     private final Activity mActivity;
73
74     private final AutoLoginDelegate mAutoLoginDelegate;
75
76     // Whether the infobar are shown on top (below the location bar) or at the bottom of the screen.
77     private final boolean mInfoBarsOnTop;
78
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>();
81
82     // We only animate changing infobars one at a time.
83     private final ArrayDeque<InfoBarTransitionInfo> mInfoBarTransitions;
84
85     // Animation currently moving InfoBars around.
86     private AnimationHelper mAnimation;
87     private final FrameLayout mAnimationSizer;
88
89     // True when this container has been emptied and its native counterpart has been destroyed.
90     private boolean mDestroyed = false;
91
92     // The id of the tab associated with us. Set to TabBase.INVALID_TAB_ID if no tab is associated.
93     private int mTabId;
94
95     // Parent view that contains us.
96     private ViewGroup mParentView;
97
98     public InfoBarContainer(Activity activity, AutoLoginProcessor autoLoginProcessor,
99             int tabId, ViewGroup parentView, int nativeWebContents) {
100         super(activity);
101         setOrientation(LinearLayout.VERTICAL);
102         mAnimationListener = null;
103         mInfoBarTransitions = new ArrayDeque<InfoBarTransitionInfo>();
104
105         mAutoLoginDelegate = new AutoLoginDelegate(autoLoginProcessor, activity);
106         mActivity = activity;
107         mTabId = tabId;
108         mParentView = parentView;
109
110         mAnimationSizer = new FrameLayout(activity);
111         mAnimationSizer.setVisibility(INVISIBLE);
112
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());
116
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);
120     }
121
122     public void setAnimationListener(InfoBarAnimationListener listener) {
123         mAnimationListener = listener;
124     }
125
126     @VisibleForTesting
127     public InfoBarAnimationListener getAnimationListener() {
128         return mAnimationListener;
129     }
130
131
132     public boolean areInfoBarsOnTop() {
133         return mInfoBarsOnTop;
134     }
135
136     @Override
137     public boolean onInterceptTouchEvent(MotionEvent ev) {
138         // Trap any attempts to fiddle with the Views while we're animating.
139         return mAnimation != null;
140     }
141
142     @Override
143     public boolean onTouchEvent(MotionEvent event) {
144         // Consume all motion events so they do not reach the ContentView.
145         return true;
146     }
147
148     private void addToParentView() {
149         if (mParentView != null && mParentView.indexOfChild(this) == -1) {
150             mParentView.addView(this, createLayoutParams());
151         }
152     }
153
154     private int determineGravity() {
155         return mInfoBarsOnTop ? Gravity.TOP : Gravity.BOTTOM;
156     }
157
158     private FrameLayout.LayoutParams createLayoutParams() {
159         return new FrameLayout.LayoutParams(
160                 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, determineGravity());
161     }
162
163     public void removeFromParentView() {
164         if (getParent() != null) {
165             ((ViewGroup) getParent()).removeView(this);
166         }
167     }
168
169     /**
170      * Called when the parent {@link android.view.ViewGroup} has changed for
171      * this container.
172      */
173     public void onParentViewChanged(int tabId, ViewGroup parentView) {
174         mTabId = tabId;
175         mParentView = parentView;
176
177         if (getParent() != null) {
178             removeFromParentView();
179             addToParentView();
180         }
181     }
182
183     @Override
184     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
185         if (mAnimation == null || child != mAnimation.getTarget()) {
186             return super.drawChild(canvas, child, drawingTime);
187         }
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
192         // paint over.
193         boolean retVal;
194         canvas.save();
195         canvas.clipRect(mAnimation.getTarget().getClippingRect());
196         retVal = super.drawChild(canvas, child, drawingTime);
197         canvas.restore();
198         return retVal;
199     }
200
201     @Override
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);
206     }
207
208     @Override
209     protected void onDetachedFromWindow() {
210         super.onDetachedFromWindow();
211         setVisibility(INVISIBLE);
212     }
213
214     public InfoBar findInfoBar(int nativeInfoBar) {
215         for (InfoBar infoBar : mInfoBars) {
216             if (infoBar.ownsNativeInfoBar(nativeInfoBar)) {
217                 return infoBar;
218             }
219         }
220         return null;
221     }
222
223     /**
224      * Adds an InfoBar to the view hierarchy.
225      * @param infoBar InfoBar to add to the View hierarchy.
226      */
227     @CalledByNative
228     public void addInfoBar(InfoBar infoBar) {
229         assert !mDestroyed;
230         if (infoBar == null) {
231             return;
232         }
233         if (mInfoBars.contains(infoBar)) {
234             assert false : "Trying to add an info bar that has already been added.";
235             return;
236         }
237
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);
244
245         enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_SHOW);
246     }
247
248     /**
249      * Returns the latest InfoBarTransitionInfo that deals with the given InfoBar.
250      * @param toFind InfoBar that we're looking for.
251      */
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;
257         }
258         return null;
259     }
260
261     /**
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.
266      */
267     public void swapInfoBarViews(InfoBar infoBar, View toShow) {
268         assert !mDestroyed;
269
270         if (!mInfoBars.contains(infoBar)) {
271             assert false : "Trying to swap an InfoBar that is not in this container.";
272             return;
273         }
274
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.";
278             return;
279         }
280
281         enqueueInfoBarAnimation(infoBar, toShow, AnimationHelper.ANIMATION_TYPE_SWAP);
282     }
283
284     /**
285      * Removes an InfoBar from the view hierarchy.
286      * @param infoBar InfoBar to remove from the View hierarchy.
287      */
288     public void removeInfoBar(InfoBar infoBar) {
289         assert !mDestroyed;
290
291         if (!mInfoBars.remove(infoBar)) {
292             assert false : "Trying to remove an InfoBar that is not in this container.";
293             return;
294         }
295
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;
308                 }
309                 if (collapseAnimations) {
310                     mInfoBarTransitions.remove(info);
311                 }
312             }
313         }
314
315         if (!collapseAnimations) {
316             enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_HIDE);
317         }
318     }
319
320     /**
321      * Enqueue a new animation to run and kicks off the animation sequence.
322      */
323     private void enqueueInfoBarAnimation(InfoBar infoBar, View toShow, int animationType) {
324         InfoBarTransitionInfo info = new InfoBarTransitionInfo(infoBar, toShow, animationType);
325         mInfoBarTransitions.add(info);
326         processPendingInfoBars();
327     }
328
329     @Override
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)) {
334             if (isShowing) {
335                 setVisibility(View.INVISIBLE);
336             }
337         } else {
338             if (!isShowing) {
339                 setVisibility(View.VISIBLE);
340             }
341         }
342         super.onLayout(changed, l, t, r, b);
343     }
344
345     /**
346      * @return True when this container has been emptied and its native counterpart has been
347      *         destroyed.
348      */
349     public boolean hasBeenDestroyed() {
350         return mDestroyed;
351     }
352
353     private void processPendingInfoBars() {
354         if (mAnimation != null || mInfoBarTransitions.isEmpty()) return;
355
356         // Start animating what has to be animated.
357         InfoBarTransitionInfo info = mInfoBarTransitions.remove();
358         View toShow = info.toShow;
359         ContentWrapperView targetView;
360
361         addToParentView();
362
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));
369         } else {
370             targetView = info.target.getContentWrapper(false);
371         }
372
373         // Kick off the animation.
374         mAnimation = new AnimationHelper(this, targetView, info.target, toShow, info.animationType);
375         mAnimation.start();
376     }
377
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>();
381
382         for (InfoBar infoBar : mInfoBars) {
383             if (infoBar.shouldExpire(url)) {
384                 barsToRemove.add(infoBar);
385             }
386         }
387
388         for (InfoBar infoBar : barsToRemove) {
389             infoBar.dismissJavaOnlyInfoBar();
390         }
391     }
392
393     /**
394      * Returns the id of the tab we are associated with.
395      */
396     public int getTabId() {
397         return mTabId;
398     }
399
400     public void destroy() {
401         mDestroyed = true;
402         removeAllViews();
403         if (mNativeInfoBarContainer != 0) {
404             nativeDestroy(mNativeInfoBarContainer);
405         }
406         mInfoBarTransitions.clear();
407     }
408
409     /**
410      * @return all of the InfoBars held in this container.
411      */
412     @VisibleForTesting
413     public ArrayList<InfoBar> getInfoBars() {
414         return mInfoBars;
415     }
416
417     /**
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
425      *               {@code false}.
426      */
427     public void processAutoLogin(String accountName, String authToken, boolean success,
428             String result) {
429         mAutoLoginDelegate.dismissAutoLogins(accountName, authToken, success, result);
430     }
431
432     /**
433      * Dismiss all auto logins infobars without processing any result.
434      */
435     public void dismissAutoLoginInfoBars() {
436         mAutoLoginDelegate.dismissAutoLogins("", "", false, "");
437     }
438
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);
446
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();
452         }
453     }
454
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);
460         }
461     }
462
463     /**
464      * Finishes off whatever animation is running.
465      */
466     public void finishTransition() {
467         assert mAnimation != null;
468
469         // If the InfoBar was hidden, get rid of its View entirely.
470         if (mAnimation.getAnimationType() == AnimationHelper.ANIMATION_TYPE_HIDE) {
471             removeView(mAnimation.getTarget());
472         }
473
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);
478         }
479         requestLayout();
480
481         // If there are no infobars shown, there is no need to keep the infobar container in the
482         // view hierarchy.
483         if (getChildCount() == 0) {
484             removeFromParentView();
485         }
486
487         if (mAnimationSizer.getParent() != null) {
488             ((ViewGroup) mAnimationSizer.getParent()).removeView(mAnimationSizer);
489         }
490
491         // Notify interested parties and move on to the next animation.
492         if (mAnimationListener != null) {
493             mAnimationListener.notifyAnimationFinished(mAnimation.getAnimationType());
494         }
495         mAnimation = null;
496         processPendingInfoBars();
497     }
498
499     /**
500      * Searches a given view's child views for an instance of {@link InfoBarContainer}.
501      *
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}.
505      */
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);
510             }
511         }
512         return null;
513     }
514
515     public int getNative() {
516         return mNativeInfoBarContainer;
517     }
518
519     private native int nativeInit(int webContentsPtr, AutoLoginDelegate autoLoginDelegate);
520
521     private native void nativeDestroy(int nativeInfoBarContainerAndroid);
522 }