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 } 593