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