Upstream version 9.38.198.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.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 com.google.common.annotations.VisibleForTesting;
20
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;
26
27 import java.util.ArrayDeque;
28 import java.util.ArrayList;
29 import java.util.Iterator;
30 import java.util.LinkedList;
31
32
33 /**
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.
38  */
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;
44
45     /**
46      * A listener for the InfoBar animation.
47      */
48     public interface InfoBarAnimationListener {
49         /**
50          * Notifies the subscriber when an animation is completed.
51          */
52         void notifyAnimationFinished(int animationType);
53     }
54
55     private static class InfoBarTransitionInfo {
56         // InfoBar being animated.
57         public InfoBar target;
58
59         // View to replace the current View shown by the ContentWrapperView.
60         public View toShow;
61
62         // Which type of animation needs to be performed.
63         public int animationType;
64
65         public InfoBarTransitionInfo(InfoBar bar, View view, int type) {
66             assert type >= AnimationHelper.ANIMATION_TYPE_SHOW;
67             assert type < AnimationHelper.ANIMATION_TYPE_BOUNDARY;
68
69             target = bar;
70             toShow = view;
71             animationType = type;
72         }
73     }
74
75     private InfoBarAnimationListener mAnimationListener;
76
77     // Native InfoBarContainer pointer which will be set by nativeInit()
78     private long mNativeInfoBarContainer;
79
80     private final Activity mActivity;
81
82     private final AutoLoginDelegate mAutoLoginDelegate;
83
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>();
86
87     // We only animate changing infobars one at a time.
88     private final ArrayDeque<InfoBarTransitionInfo> mInfoBarTransitions;
89
90     // Animation currently moving InfoBars around.
91     private AnimationHelper mAnimation;
92     private final FrameLayout mAnimationSizer;
93
94     // True when this container has been emptied and its native counterpart has been destroyed.
95     private boolean mDestroyed = false;
96
97     // The id of the tab associated with us. Set to Tab.INVALID_TAB_ID if no tab is associated.
98     private int mTabId;
99
100     // Parent view that contains us.
101     private ViewGroup mParentView;
102
103     // The LinearLayout that holds the infobars. This is the only child of the InfoBarContainer.
104     private LinearLayout mLinearLayout;
105
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.
108     private int mHeight;
109     private int mInnerHeight;
110     private int mDistanceFromBottom;
111
112     private Paint mTopBorderPaint;
113
114     public InfoBarContainer(Activity activity, AutoLoginProcessor autoLoginProcessor,
115             int tabId, ViewGroup parentView, WebContents webContents) {
116         super(activity);
117
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);
124         setLayoutParams(lp);
125
126         mLinearLayout = new LinearLayout(activity);
127         mLinearLayout.setOrientation(LinearLayout.VERTICAL);
128         addView(mLinearLayout,
129                 new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
130
131         mAnimationListener = null;
132         mInfoBarTransitions = new ArrayDeque<InfoBarTransitionInfo>();
133
134         mAutoLoginDelegate = new AutoLoginDelegate(autoLoginProcessor, activity);
135         mActivity = activity;
136         mTabId = tabId;
137         mParentView = parentView;
138
139         mAnimationSizer = new FrameLayout(activity);
140         mAnimationSizer.setVisibility(INVISIBLE);
141
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);
145     }
146
147     /**
148      * @return The LinearLayout that holds the infobars (i.e. the ContentWrapperViews).
149      */
150     LinearLayout getLinearLayout() {
151         return mLinearLayout;
152     }
153
154     public void setAnimationListener(InfoBarAnimationListener listener) {
155         mAnimationListener = listener;
156     }
157
158     @VisibleForTesting
159     public InfoBarAnimationListener getAnimationListener() {
160         return mAnimationListener;
161     }
162
163     @Override
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;
167     }
168
169     private void addToParentView() {
170         if (mParentView != null && mParentView.indexOfChild(this) == -1) {
171             mParentView.addView(this);
172         }
173     }
174
175     public void removeFromParentView() {
176         if (getParent() != null) {
177             ((ViewGroup) getParent()).removeView(this);
178         }
179     }
180
181     /**
182      * Called when the parent {@link android.view.ViewGroup} has changed for
183      * this container.
184      */
185     public void onParentViewChanged(int tabId, ViewGroup parentView) {
186         mTabId = tabId;
187         mParentView = parentView;
188
189         removeFromParentView();
190         addToParentView();
191     }
192
193     @Override
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);
198     }
199
200     @Override
201     protected void onDetachedFromWindow() {
202         super.onDetachedFromWindow();
203         setVisibility(INVISIBLE);
204     }
205
206     /**
207      * Adds an InfoBar to the view hierarchy.
208      * @param infoBar InfoBar to add to the View hierarchy.
209      */
210     @CalledByNative
211     public void addInfoBar(InfoBar infoBar) {
212         assert !mDestroyed;
213         if (infoBar == null) {
214             return;
215         }
216         if (mInfoBars.contains(infoBar)) {
217             assert false : "Trying to add an info bar that has already been added.";
218             return;
219         }
220
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);
227
228         enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_SHOW);
229     }
230
231     /**
232      * Returns the latest InfoBarTransitionInfo that deals with the given InfoBar.
233      * @param toFind InfoBar that we're looking for.
234      */
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;
240         }
241         return null;
242     }
243
244     /**
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.
249      */
250     public void swapInfoBarViews(InfoBar infoBar, View toShow) {
251         assert !mDestroyed;
252
253         if (!mInfoBars.contains(infoBar)) {
254             assert false : "Trying to swap an InfoBar that is not in this container.";
255             return;
256         }
257
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.";
261             return;
262         }
263
264         enqueueInfoBarAnimation(infoBar, toShow, AnimationHelper.ANIMATION_TYPE_SWAP);
265     }
266
267     /**
268      * Removes an InfoBar from the view hierarchy.
269      * @param infoBar InfoBar to remove from the View hierarchy.
270      */
271     public void removeInfoBar(InfoBar infoBar) {
272         assert !mDestroyed;
273
274         if (!mInfoBars.remove(infoBar)) {
275             assert false : "Trying to remove an InfoBar that is not in this container.";
276             return;
277         }
278
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;
291                 }
292                 if (collapseAnimations) {
293                     mInfoBarTransitions.remove(info);
294                 }
295             }
296         }
297
298         if (!collapseAnimations) {
299             enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_HIDE);
300         }
301     }
302
303     /**
304      * Enqueue a new animation to run and kicks off the animation sequence.
305      */
306     private void enqueueInfoBarAnimation(InfoBar infoBar, View toShow, int animationType) {
307         InfoBarTransitionInfo info = new InfoBarTransitionInfo(infoBar, toShow, animationType);
308         mInfoBarTransitions.add(info);
309         processPendingInfoBars();
310     }
311
312     @Override
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)) {
317             if (isShowing) {
318                 setVisibility(View.INVISIBLE);
319             }
320         } else {
321             if (!isShowing) {
322                 setVisibility(View.VISIBLE);
323             }
324         }
325         super.onLayout(changed, l, t, r, b);
326
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);
334         }
335         mHeight = newHeight;
336         mInnerHeight = newInnerHeight;
337         mDistanceFromBottom = mInnerHeight - mHeight - getScrollY();
338     }
339
340     @Override
341     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
342         super.onScrollChanged(l, t, oldl, oldt);
343         mDistanceFromBottom = mInnerHeight - mHeight - getScrollY();
344     }
345
346     @Override
347     protected void dispatchDraw(Canvas canvas) {
348         super.dispatchDraw(canvas);
349
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
352         // is active.
353         if (getScrollY() != 0) {
354             if (mTopBorderPaint == null) {
355                 mTopBorderPaint = new Paint();
356                 mTopBorderPaint.setColor(
357                         getResources().getColor(R.color.infobar_background_separator));
358             }
359             int height = ContentWrapperView.getBoundaryHeight(getContext());
360             canvas.drawRect(0, getScrollY(), getWidth(), getScrollY() + height, mTopBorderPaint);
361         }
362     }
363
364     /**
365      * @return True when this container has been emptied and its native counterpart has been
366      *         destroyed.
367      */
368     public boolean hasBeenDestroyed() {
369         return mDestroyed;
370     }
371
372     private void processPendingInfoBars() {
373         if (mAnimation != null || mInfoBarTransitions.isEmpty()) return;
374
375         // Start animating what has to be animated.
376         InfoBarTransitionInfo info = mInfoBarTransitions.remove();
377         View toShow = info.toShow;
378         ContentWrapperView targetView;
379
380         addToParentView();
381
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));
388         } else {
389             targetView = info.target.getContentWrapper(false);
390         }
391
392         // Kick off the animation.
393         mAnimation = new AnimationHelper(this, targetView, info.target, toShow, info.animationType);
394         mAnimation.start();
395     }
396
397     // Called by the tab when it has started loading a new page.
398     public void onPageStarted() {
399         LinkedList<InfoBar> barsToRemove = new LinkedList<InfoBar>();
400
401         for (InfoBar infoBar : mInfoBars) {
402             if (infoBar.shouldExpire()) {
403                 barsToRemove.add(infoBar);
404             }
405         }
406
407         for (InfoBar infoBar : barsToRemove) {
408             infoBar.dismissJavaOnlyInfoBar();
409         }
410     }
411
412     /**
413      * Returns the id of the tab we are associated with.
414      */
415     public int getTabId() {
416         return mTabId;
417     }
418
419     public void destroy() {
420         mDestroyed = true;
421         mLinearLayout.removeAllViews();
422         if (mNativeInfoBarContainer != 0) {
423             nativeDestroy(mNativeInfoBarContainer);
424         }
425         mInfoBarTransitions.clear();
426     }
427
428     /**
429      * @return all of the InfoBars held in this container.
430      */
431     @VisibleForTesting
432     public ArrayList<InfoBar> getInfoBars() {
433         return mInfoBars;
434     }
435
436     /**
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
444      *               {@code false}.
445      */
446     public void processAutoLogin(String accountName, String authToken, boolean success,
447             String result) {
448         mAutoLoginDelegate.dismissAutoLogins(accountName, authToken, success, result);
449     }
450
451     /**
452      * Dismiss all auto logins infobars without processing any result.
453      */
454     public void dismissAutoLoginInfoBars() {
455         mAutoLoginDelegate.dismissAutoLogins("", "", false, "");
456     }
457
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);
465
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();
472         }
473     }
474
475     /**
476      * Finishes off whatever animation is running.
477      */
478     public void finishTransition() {
479         assert mAnimation != null;
480
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());
484         }
485
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);
490         }
491         requestLayout();
492
493         // If there are no infobars shown, there is no need to keep the infobar container in the
494         // view hierarchy.
495         if (mLinearLayout.getChildCount() == 0) {
496             removeFromParentView();
497         }
498
499         if (mAnimationSizer.getParent() != null) {
500             ((ViewGroup) mAnimationSizer.getParent()).removeView(mAnimationSizer);
501         }
502
503         // Notify interested parties and move on to the next animation.
504         if (mAnimationListener != null) {
505             mAnimationListener.notifyAnimationFinished(mAnimation.getAnimationType());
506         }
507         mAnimation = null;
508         processPendingInfoBars();
509     }
510
511     /**
512      * Searches a given view's child views for an instance of {@link InfoBarContainer}.
513      *
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}.
517      */
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);
522             }
523         }
524         return null;
525     }
526
527     public long getNative() {
528         return mNativeInfoBarContainer;
529     }
530
531     private native long nativeInit(WebContents webContents, AutoLoginDelegate autoLoginDelegate);
532     private native void nativeDestroy(long nativeInfoBarContainerAndroid);
533 }