Upstream version 11.39.250.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 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 }