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