1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.statusbar; 18 19 import android.app.Notification; 20 import android.app.PendingIntent; 21 import android.app.RemoteInput; 22 import android.content.Context; 23 import android.graphics.Rect; 24 import android.os.Build; 25 import android.service.notification.StatusBarNotification; 26 import android.util.AttributeSet; 27 import android.view.NotificationHeaderView; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.ViewTreeObserver; 31 import android.widget.FrameLayout; 32 import android.widget.ImageView; 33 34 import com.android.internal.util.NotificationColorUtil; 35 import com.android.systemui.R; 36 import com.android.systemui.statusbar.notification.HybridNotificationView; 37 import com.android.systemui.statusbar.notification.HybridGroupManager; 38 import com.android.systemui.statusbar.notification.NotificationCustomViewWrapper; 39 import com.android.systemui.statusbar.notification.NotificationUtils; 40 import com.android.systemui.statusbar.notification.NotificationViewWrapper; 41 import com.android.systemui.statusbar.phone.NotificationGroupManager; 42 import com.android.systemui.statusbar.policy.RemoteInputView; 43 44 /** 45 * A frame layout containing the actual payload of the notification, including the contracted, 46 * expanded and heads up layout. This class is responsible for clipping the content and and 47 * switching between the expanded, contracted and the heads up view depending on its clipped size. 48 */ 49 public class NotificationContentView extends FrameLayout { 50 51 private static final int VISIBLE_TYPE_CONTRACTED = 0; 52 private static final int VISIBLE_TYPE_EXPANDED = 1; 53 private static final int VISIBLE_TYPE_HEADSUP = 2; 54 private static final int VISIBLE_TYPE_SINGLELINE = 3; 55 public static final int UNDEFINED = -1; 56 57 private final Rect mClipBounds = new Rect(); 58 private final int mMinContractedHeight; 59 private final int mNotificationContentMarginEnd; 60 61 private View mContractedChild; 62 private View mExpandedChild; 63 private View mHeadsUpChild; 64 private HybridNotificationView mSingleLineView; 65 66 private RemoteInputView mExpandedRemoteInput; 67 private RemoteInputView mHeadsUpRemoteInput; 68 69 private NotificationViewWrapper mContractedWrapper; 70 private NotificationViewWrapper mExpandedWrapper; 71 private NotificationViewWrapper mHeadsUpWrapper; 72 private HybridGroupManager mHybridGroupManager; 73 private int mClipTopAmount; 74 private int mContentHeight; 75 private int mVisibleType = VISIBLE_TYPE_CONTRACTED; 76 private boolean mDark; 77 private boolean mAnimate; 78 private boolean mIsHeadsUp; 79 private boolean mShowingLegacyBackground; 80 private boolean mIsChildInGroup; 81 private int mSmallHeight; 82 private int mHeadsUpHeight; 83 private int mNotificationMaxHeight; 84 private StatusBarNotification mStatusBarNotification; 85 private NotificationGroupManager mGroupManager; 86 private RemoteInputController mRemoteInputController; 87 88 private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener 89 = new ViewTreeObserver.OnPreDrawListener() { 90 @Override 91 public boolean onPreDraw() { 92 // We need to post since we don't want the notification to animate on the very first 93 // frame 94 post(new Runnable() { 95 @Override 96 public void run() { 97 mAnimate = true; 98 } 99 }); 100 getViewTreeObserver().removeOnPreDrawListener(this); 101 return true; 102 } 103 }; 104 105 private OnClickListener mExpandClickListener; 106 private boolean mBeforeN; 107 private boolean mExpandable; 108 private boolean mClipToActualHeight = true; 109 private ExpandableNotificationRow mContainingNotification; 110 /** The visible type at the start of a touch driven transformation */ 111 private int mTransformationStartVisibleType; 112 /** The visible type at the start of an animation driven transformation */ 113 private int mAnimationStartVisibleType = UNDEFINED; 114 private boolean mUserExpanding; 115 private int mSingleLineWidthIndention; 116 private boolean mForceSelectNextLayout = true; 117 private PendingIntent mPreviousExpandedRemoteInputIntent; 118 private PendingIntent mPreviousHeadsUpRemoteInputIntent; 119 private RemoteInputView mCachedExpandedRemoteInput; 120 private RemoteInputView mCachedHeadsUpRemoteInput; 121 122 private int mContentHeightAtAnimationStart = UNDEFINED; 123 private boolean mFocusOnVisibilityChange; 124 private boolean mHeadsupDisappearRunning; 125 126 127 public NotificationContentView(Context context, AttributeSet attrs) { 128 super(context, attrs); 129 mHybridGroupManager = new HybridGroupManager(getContext(), this); 130 mMinContractedHeight = getResources().getDimensionPixelSize( 131 R.dimen.min_notification_layout_height); 132 mNotificationContentMarginEnd = getResources().getDimensionPixelSize( 133 com.android.internal.R.dimen.notification_content_margin_end); 134 reset(); 135 } 136 137 public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight) { 138 mSmallHeight = smallHeight; 139 mHeadsUpHeight = headsUpMaxHeight; 140 mNotificationMaxHeight = maxHeight; 141 } 142 143 @Override 144 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 145 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 146 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 147 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 148 int maxSize = Integer.MAX_VALUE; 149 int width = MeasureSpec.getSize(widthMeasureSpec); 150 if (hasFixedHeight || isHeightLimited) { 151 maxSize = MeasureSpec.getSize(heightMeasureSpec); 152 } 153 int maxChildHeight = 0; 154 if (mExpandedChild != null) { 155 int size = Math.min(maxSize, mNotificationMaxHeight); 156 ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams(); 157 if (layoutParams.height >= 0) { 158 // An actual height is set 159 size = Math.min(maxSize, layoutParams.height); 160 } 161 int spec = size == Integer.MAX_VALUE 162 ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) 163 : MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); 164 mExpandedChild.measure(widthMeasureSpec, spec); 165 maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight()); 166 } 167 if (mContractedChild != null) { 168 int heightSpec; 169 int size = Math.min(maxSize, mSmallHeight); 170 if (shouldContractedBeFixedSize()) { 171 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 172 } else { 173 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); 174 } 175 mContractedChild.measure(widthMeasureSpec, heightSpec); 176 int measuredHeight = mContractedChild.getMeasuredHeight(); 177 if (measuredHeight < mMinContractedHeight) { 178 heightSpec = MeasureSpec.makeMeasureSpec(mMinContractedHeight, MeasureSpec.EXACTLY); 179 mContractedChild.measure(widthMeasureSpec, heightSpec); 180 } 181 maxChildHeight = Math.max(maxChildHeight, measuredHeight); 182 if (updateContractedHeaderWidth()) { 183 mContractedChild.measure(widthMeasureSpec, heightSpec); 184 } 185 if (mExpandedChild != null 186 && mContractedChild.getMeasuredHeight() > mExpandedChild.getMeasuredHeight()) { 187 // the Expanded child is smaller then the collapsed. Let's remeasure it. 188 heightSpec = MeasureSpec.makeMeasureSpec(mContractedChild.getMeasuredHeight(), 189 MeasureSpec.EXACTLY); 190 mExpandedChild.measure(widthMeasureSpec, heightSpec); 191 } 192 } 193 if (mHeadsUpChild != null) { 194 int size = Math.min(maxSize, mHeadsUpHeight); 195 ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams(); 196 if (layoutParams.height >= 0) { 197 // An actual height is set 198 size = Math.min(size, layoutParams.height); 199 } 200 mHeadsUpChild.measure(widthMeasureSpec, 201 MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST)); 202 maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight()); 203 } 204 if (mSingleLineView != null) { 205 int singleLineWidthSpec = widthMeasureSpec; 206 if (mSingleLineWidthIndention != 0 207 && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { 208 singleLineWidthSpec = MeasureSpec.makeMeasureSpec( 209 width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(), 210 MeasureSpec.EXACTLY); 211 } 212 mSingleLineView.measure(singleLineWidthSpec, 213 MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST)); 214 maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight()); 215 } 216 int ownHeight = Math.min(maxChildHeight, maxSize); 217 setMeasuredDimension(width, ownHeight); 218 } 219 220 private boolean updateContractedHeaderWidth() { 221 // We need to update the expanded and the collapsed header to have exactly the same with to 222 // have the expand buttons laid out at the same location. 223 NotificationHeaderView contractedHeader = mContractedWrapper.getNotificationHeader(); 224 if (contractedHeader != null) { 225 if (mExpandedChild != null 226 && mExpandedWrapper.getNotificationHeader() != null) { 227 NotificationHeaderView expandedHeader = mExpandedWrapper.getNotificationHeader(); 228 int expandedSize = expandedHeader.getMeasuredWidth() 229 - expandedHeader.getPaddingEnd(); 230 int collapsedSize = contractedHeader.getMeasuredWidth() 231 - expandedHeader.getPaddingEnd(); 232 if (expandedSize != collapsedSize) { 233 int paddingEnd = contractedHeader.getMeasuredWidth() - expandedSize; 234 contractedHeader.setPadding( 235 contractedHeader.isLayoutRtl() 236 ? paddingEnd 237 : contractedHeader.getPaddingLeft(), 238 contractedHeader.getPaddingTop(), 239 contractedHeader.isLayoutRtl() 240 ? contractedHeader.getPaddingLeft() 241 : paddingEnd, 242 contractedHeader.getPaddingBottom()); 243 contractedHeader.setShowWorkBadgeAtEnd(true); 244 return true; 245 } 246 } else { 247 int paddingEnd = mNotificationContentMarginEnd; 248 if (contractedHeader.getPaddingEnd() != paddingEnd) { 249 contractedHeader.setPadding( 250 contractedHeader.isLayoutRtl() 251 ? paddingEnd 252 : contractedHeader.getPaddingLeft(), 253 contractedHeader.getPaddingTop(), 254 contractedHeader.isLayoutRtl() 255 ? contractedHeader.getPaddingLeft() 256 : paddingEnd, 257 contractedHeader.getPaddingBottom()); 258 contractedHeader.setShowWorkBadgeAtEnd(false); 259 return true; 260 } 261 } 262 } 263 return false; 264 } 265 266 private boolean shouldContractedBeFixedSize() { 267 return mBeforeN && mContractedWrapper instanceof NotificationCustomViewWrapper; 268 } 269 270 @Override 271 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 272 int previousHeight = 0; 273 if (mExpandedChild != null) { 274 previousHeight = mExpandedChild.getHeight(); 275 } 276 super.onLayout(changed, left, top, right, bottom); 277 if (previousHeight != 0 && mExpandedChild.getHeight() != previousHeight) { 278 mContentHeightAtAnimationStart = previousHeight; 279 } 280 updateClipping(); 281 invalidateOutline(); 282 selectLayout(false /* animate */, mForceSelectNextLayout /* force */); 283 mForceSelectNextLayout = false; 284 updateExpandButtons(mExpandable); 285 } 286 287 @Override 288 protected void onAttachedToWindow() { 289 super.onAttachedToWindow(); 290 updateVisibility(); 291 } 292 293 public void reset() { 294 if (mContractedChild != null) { 295 mContractedChild.animate().cancel(); 296 removeView(mContractedChild); 297 } 298 mPreviousExpandedRemoteInputIntent = null; 299 if (mExpandedRemoteInput != null) { 300 mExpandedRemoteInput.onNotificationUpdateOrReset(); 301 if (mExpandedRemoteInput.isActive()) { 302 mPreviousExpandedRemoteInputIntent = mExpandedRemoteInput.getPendingIntent(); 303 mCachedExpandedRemoteInput = mExpandedRemoteInput; 304 mExpandedRemoteInput.dispatchStartTemporaryDetach(); 305 ((ViewGroup)mExpandedRemoteInput.getParent()).removeView(mExpandedRemoteInput); 306 } 307 } 308 if (mExpandedChild != null) { 309 mExpandedChild.animate().cancel(); 310 removeView(mExpandedChild); 311 mExpandedRemoteInput = null; 312 } 313 mPreviousHeadsUpRemoteInputIntent = null; 314 if (mHeadsUpRemoteInput != null) { 315 mHeadsUpRemoteInput.onNotificationUpdateOrReset(); 316 if (mHeadsUpRemoteInput.isActive()) { 317 mPreviousHeadsUpRemoteInputIntent = mHeadsUpRemoteInput.getPendingIntent(); 318 mCachedHeadsUpRemoteInput = mHeadsUpRemoteInput; 319 mHeadsUpRemoteInput.dispatchStartTemporaryDetach(); 320 ((ViewGroup)mHeadsUpRemoteInput.getParent()).removeView(mHeadsUpRemoteInput); 321 } 322 } 323 if (mHeadsUpChild != null) { 324 mHeadsUpChild.animate().cancel(); 325 removeView(mHeadsUpChild); 326 mHeadsUpRemoteInput = null; 327 } 328 mContractedChild = null; 329 mExpandedChild = null; 330 mHeadsUpChild = null; 331 } 332 333 public View getContractedChild() { 334 return mContractedChild; 335 } 336 337 public View getExpandedChild() { 338 return mExpandedChild; 339 } 340 341 public View getHeadsUpChild() { 342 return mHeadsUpChild; 343 } 344 345 public void setContractedChild(View child) { 346 if (mContractedChild != null) { 347 mContractedChild.animate().cancel(); 348 removeView(mContractedChild); 349 } 350 addView(child); 351 mContractedChild = child; 352 mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child, 353 mContainingNotification); 354 mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */); 355 } 356 357 public void setExpandedChild(View child) { 358 if (mExpandedChild != null) { 359 mExpandedChild.animate().cancel(); 360 removeView(mExpandedChild); 361 } 362 addView(child); 363 mExpandedChild = child; 364 mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child, 365 mContainingNotification); 366 } 367 368 public void setHeadsUpChild(View child) { 369 if (mHeadsUpChild != null) { 370 mHeadsUpChild.animate().cancel(); 371 removeView(mHeadsUpChild); 372 } 373 addView(child); 374 mHeadsUpChild = child; 375 mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, 376 mContainingNotification); 377 } 378 379 @Override 380 protected void onVisibilityChanged(View changedView, int visibility) { 381 super.onVisibilityChanged(changedView, visibility); 382 updateVisibility(); 383 } 384 385 private void updateVisibility() { 386 setVisible(isShown()); 387 } 388 389 @Override 390 protected void onDetachedFromWindow() { 391 super.onDetachedFromWindow(); 392 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 393 } 394 395 private void setVisible(final boolean isVisible) { 396 if (isVisible) { 397 // This call can happen multiple times, but removing only removes a single one. 398 // We therefore need to remove the old one. 399 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 400 // We only animate if we are drawn at least once, otherwise the view might animate when 401 // it's shown the first time 402 getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener); 403 } else { 404 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 405 mAnimate = false; 406 } 407 } 408 409 private void focusExpandButtonIfNecessary() { 410 if (mFocusOnVisibilityChange) { 411 NotificationHeaderView header = getVisibleNotificationHeader(); 412 if (header != null) { 413 ImageView expandButton = header.getExpandButton(); 414 if (expandButton != null) { 415 expandButton.requestAccessibilityFocus(); 416 } 417 } 418 mFocusOnVisibilityChange = false; 419 } 420 } 421 422 public void setContentHeight(int contentHeight) { 423 mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight()); 424 selectLayout(mAnimate /* animate */, false /* force */); 425 426 int minHeightHint = getMinContentHeightHint(); 427 428 NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); 429 if (wrapper != null) { 430 wrapper.setContentHeight(mContentHeight, minHeightHint); 431 } 432 433 wrapper = getVisibleWrapper(mTransformationStartVisibleType); 434 if (wrapper != null) { 435 wrapper.setContentHeight(mContentHeight, minHeightHint); 436 } 437 438 updateClipping(); 439 invalidateOutline(); 440 } 441 442 /** 443 * @return the minimum apparent height that the wrapper should allow for the purpose 444 * of aligning elements at the bottom edge. If this is larger than the content 445 * height, the notification is clipped instead of being further shrunk. 446 */ 447 private int getMinContentHeightHint() { 448 if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) { 449 return mContext.getResources().getDimensionPixelSize( 450 com.android.internal.R.dimen.notification_action_list_height); 451 } 452 453 // Transition between heads-up & expanded, or pinned. 454 if (mHeadsUpChild != null && mExpandedChild != null) { 455 boolean transitioningBetweenHunAndExpanded = 456 isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) || 457 isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP); 458 boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED) 459 && (mIsHeadsUp || mHeadsupDisappearRunning); 460 if (transitioningBetweenHunAndExpanded || pinned) { 461 return Math.min(mHeadsUpChild.getHeight(), mExpandedChild.getHeight()); 462 } 463 } 464 465 // Size change of the expanded version 466 if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart >= 0 467 && mExpandedChild != null) { 468 return Math.min(mContentHeightAtAnimationStart, mExpandedChild.getHeight()); 469 } 470 471 int hint; 472 if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) { 473 hint = mHeadsUpChild.getHeight(); 474 } else if (mExpandedChild != null) { 475 hint = mExpandedChild.getHeight(); 476 } else { 477 hint = mContractedChild.getHeight() + mContext.getResources().getDimensionPixelSize( 478 com.android.internal.R.dimen.notification_action_list_height); 479 } 480 481 if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) { 482 hint = Math.min(hint, mExpandedChild.getHeight()); 483 } 484 return hint; 485 } 486 487 private boolean isTransitioningFromTo(int from, int to) { 488 return (mTransformationStartVisibleType == from || mAnimationStartVisibleType == from) 489 && mVisibleType == to; 490 } 491 492 private boolean isVisibleOrTransitioning(int type) { 493 return mVisibleType == type || mTransformationStartVisibleType == type 494 || mAnimationStartVisibleType == type; 495 } 496 497 private void updateContentTransformation() { 498 int visibleType = calculateVisibleType(); 499 if (visibleType != mVisibleType) { 500 // A new transformation starts 501 mTransformationStartVisibleType = mVisibleType; 502 final TransformableView shownView = getTransformableViewForVisibleType(visibleType); 503 final TransformableView hiddenView = getTransformableViewForVisibleType( 504 mTransformationStartVisibleType); 505 shownView.transformFrom(hiddenView, 0.0f); 506 getViewForVisibleType(visibleType).setVisibility(View.VISIBLE); 507 hiddenView.transformTo(shownView, 0.0f); 508 mVisibleType = visibleType; 509 updateBackgroundColor(true /* animate */); 510 } 511 if (mForceSelectNextLayout) { 512 forceUpdateVisibilities(); 513 } 514 if (mTransformationStartVisibleType != UNDEFINED 515 && mVisibleType != mTransformationStartVisibleType 516 && getViewForVisibleType(mTransformationStartVisibleType) != null) { 517 final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType); 518 final TransformableView hiddenView = getTransformableViewForVisibleType( 519 mTransformationStartVisibleType); 520 float transformationAmount = calculateTransformationAmount(); 521 shownView.transformFrom(hiddenView, transformationAmount); 522 hiddenView.transformTo(shownView, transformationAmount); 523 updateBackgroundTransformation(transformationAmount); 524 } else { 525 updateViewVisibilities(visibleType); 526 updateBackgroundColor(false); 527 } 528 } 529 530 private void updateBackgroundTransformation(float transformationAmount) { 531 int endColor = getBackgroundColor(mVisibleType); 532 int startColor = getBackgroundColor(mTransformationStartVisibleType); 533 if (endColor != startColor) { 534 if (startColor == 0) { 535 startColor = mContainingNotification.getBackgroundColorWithoutTint(); 536 } 537 if (endColor == 0) { 538 endColor = mContainingNotification.getBackgroundColorWithoutTint(); 539 } 540 endColor = NotificationUtils.interpolateColors(startColor, endColor, 541 transformationAmount); 542 } 543 mContainingNotification.updateBackgroundAlpha(transformationAmount); 544 mContainingNotification.setContentBackground(endColor, false, this); 545 } 546 547 private float calculateTransformationAmount() { 548 int startHeight = getViewForVisibleType(mTransformationStartVisibleType).getHeight(); 549 int endHeight = getViewForVisibleType(mVisibleType).getHeight(); 550 int progress = Math.abs(mContentHeight - startHeight); 551 int totalDistance = Math.abs(endHeight - startHeight); 552 float amount = (float) progress / (float) totalDistance; 553 return Math.min(1.0f, amount); 554 } 555 556 public int getContentHeight() { 557 return mContentHeight; 558 } 559 560 public int getMaxHeight() { 561 if (mExpandedChild != null) { 562 return mExpandedChild.getHeight(); 563 } else if (mIsHeadsUp && mHeadsUpChild != null) { 564 return mHeadsUpChild.getHeight(); 565 } 566 return mContractedChild.getHeight(); 567 } 568 569 public int getMinHeight() { 570 return getMinHeight(false /* likeGroupExpanded */); 571 } 572 573 public int getMinHeight(boolean likeGroupExpanded) { 574 if (likeGroupExpanded || !mIsChildInGroup || isGroupExpanded()) { 575 return mContractedChild.getHeight(); 576 } else { 577 return mSingleLineView.getHeight(); 578 } 579 } 580 581 private boolean isGroupExpanded() { 582 return mGroupManager.isGroupExpanded(mStatusBarNotification); 583 } 584 585 public void setClipTopAmount(int clipTopAmount) { 586 mClipTopAmount = clipTopAmount; 587 updateClipping(); 588 } 589 590 private void updateClipping() { 591 if (mClipToActualHeight) { 592 mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight); 593 setClipBounds(mClipBounds); 594 } else { 595 setClipBounds(null); 596 } 597 } 598 599 public void setClipToActualHeight(boolean clipToActualHeight) { 600 mClipToActualHeight = clipToActualHeight; 601 updateClipping(); 602 } 603 604 private void selectLayout(boolean animate, boolean force) { 605 if (mContractedChild == null) { 606 return; 607 } 608 if (mUserExpanding) { 609 updateContentTransformation(); 610 } else { 611 int visibleType = calculateVisibleType(); 612 boolean changedType = visibleType != mVisibleType; 613 if (changedType || force) { 614 View visibleView = getViewForVisibleType(visibleType); 615 if (visibleView != null) { 616 visibleView.setVisibility(VISIBLE); 617 transferRemoteInputFocus(visibleType); 618 } 619 NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType); 620 if (visibleWrapper != null) { 621 visibleWrapper.setContentHeight(mContentHeight, getMinContentHeightHint()); 622 } 623 624 if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null) 625 || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null) 626 || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null) 627 || visibleType == VISIBLE_TYPE_CONTRACTED)) { 628 animateToVisibleType(visibleType); 629 } else { 630 updateViewVisibilities(visibleType); 631 } 632 mVisibleType = visibleType; 633 if (changedType) { 634 focusExpandButtonIfNecessary(); 635 } 636 updateBackgroundColor(animate); 637 } 638 } 639 } 640 641 private void forceUpdateVisibilities() { 642 boolean contractedVisible = mVisibleType == VISIBLE_TYPE_CONTRACTED 643 || mTransformationStartVisibleType == VISIBLE_TYPE_CONTRACTED; 644 boolean expandedVisible = mVisibleType == VISIBLE_TYPE_EXPANDED 645 || mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED; 646 boolean headsUpVisible = mVisibleType == VISIBLE_TYPE_HEADSUP 647 || mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP; 648 boolean singleLineVisible = mVisibleType == VISIBLE_TYPE_SINGLELINE 649 || mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE; 650 if (!contractedVisible) { 651 mContractedChild.setVisibility(View.INVISIBLE); 652 } else { 653 mContractedWrapper.setVisible(true); 654 } 655 if (mExpandedChild != null) { 656 if (!expandedVisible) { 657 mExpandedChild.setVisibility(View.INVISIBLE); 658 } else { 659 mExpandedWrapper.setVisible(true); 660 } 661 } 662 if (mHeadsUpChild != null) { 663 if (!headsUpVisible) { 664 mHeadsUpChild.setVisibility(View.INVISIBLE); 665 } else { 666 mHeadsUpWrapper.setVisible(true); 667 } 668 } 669 if (mSingleLineView != null) { 670 if (!singleLineVisible) { 671 mSingleLineView.setVisibility(View.INVISIBLE); 672 } else { 673 mSingleLineView.setVisible(true); 674 } 675 } 676 } 677 678 public void updateBackgroundColor(boolean animate) { 679 int customBackgroundColor = getBackgroundColor(mVisibleType); 680 mContainingNotification.resetBackgroundAlpha(); 681 mContainingNotification.setContentBackground(customBackgroundColor, animate, this); 682 } 683 684 public int getVisibleType() { 685 return mVisibleType; 686 } 687 688 public int getBackgroundColorForExpansionState() { 689 // When expanding or user locked we want the new type, when collapsing we want 690 // the original type 691 final int visibleType = (mContainingNotification.isGroupExpanded() 692 || mContainingNotification.isUserLocked()) 693 ? calculateVisibleType() 694 : getVisibleType(); 695 return getBackgroundColor(visibleType); 696 } 697 698 public int getBackgroundColor(int visibleType) { 699 NotificationViewWrapper currentVisibleWrapper = getVisibleWrapper(visibleType); 700 int customBackgroundColor = 0; 701 if (currentVisibleWrapper != null) { 702 customBackgroundColor = currentVisibleWrapper.getCustomBackgroundColor(); 703 } 704 return customBackgroundColor; 705 } 706 707 private void updateViewVisibilities(int visibleType) { 708 boolean contractedVisible = visibleType == VISIBLE_TYPE_CONTRACTED; 709 mContractedWrapper.setVisible(contractedVisible); 710 if (mExpandedChild != null) { 711 boolean expandedVisible = visibleType == VISIBLE_TYPE_EXPANDED; 712 mExpandedWrapper.setVisible(expandedVisible); 713 } 714 if (mHeadsUpChild != null) { 715 boolean headsUpVisible = visibleType == VISIBLE_TYPE_HEADSUP; 716 mHeadsUpWrapper.setVisible(headsUpVisible); 717 } 718 if (mSingleLineView != null) { 719 boolean singleLineVisible = visibleType == VISIBLE_TYPE_SINGLELINE; 720 mSingleLineView.setVisible(singleLineVisible); 721 } 722 } 723 724 private void animateToVisibleType(int visibleType) { 725 final TransformableView shownView = getTransformableViewForVisibleType(visibleType); 726 final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType); 727 if (shownView == hiddenView || hiddenView == null) { 728 shownView.setVisible(true); 729 return; 730 } 731 mAnimationStartVisibleType = mVisibleType; 732 shownView.transformFrom(hiddenView); 733 getViewForVisibleType(visibleType).setVisibility(View.VISIBLE); 734 hiddenView.transformTo(shownView, new Runnable() { 735 @Override 736 public void run() { 737 if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) { 738 hiddenView.setVisible(false); 739 } 740 mAnimationStartVisibleType = UNDEFINED; 741 } 742 }); 743 } 744 745 private void transferRemoteInputFocus(int visibleType) { 746 if (visibleType == VISIBLE_TYPE_HEADSUP 747 && mHeadsUpRemoteInput != null 748 && (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive())) { 749 mHeadsUpRemoteInput.stealFocusFrom(mExpandedRemoteInput); 750 } 751 if (visibleType == VISIBLE_TYPE_EXPANDED 752 && mExpandedRemoteInput != null 753 && (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive())) { 754 mExpandedRemoteInput.stealFocusFrom(mHeadsUpRemoteInput); 755 } 756 } 757 758 /** 759 * @param visibleType one of the static enum types in this view 760 * @return the corresponding transformable view according to the given visible type 761 */ 762 private TransformableView getTransformableViewForVisibleType(int visibleType) { 763 switch (visibleType) { 764 case VISIBLE_TYPE_EXPANDED: 765 return mExpandedWrapper; 766 case VISIBLE_TYPE_HEADSUP: 767 return mHeadsUpWrapper; 768 case VISIBLE_TYPE_SINGLELINE: 769 return mSingleLineView; 770 default: 771 return mContractedWrapper; 772 } 773 } 774 775 /** 776 * @param visibleType one of the static enum types in this view 777 * @return the corresponding view according to the given visible type 778 */ 779 private View getViewForVisibleType(int visibleType) { 780 switch (visibleType) { 781 case VISIBLE_TYPE_EXPANDED: 782 return mExpandedChild; 783 case VISIBLE_TYPE_HEADSUP: 784 return mHeadsUpChild; 785 case VISIBLE_TYPE_SINGLELINE: 786 return mSingleLineView; 787 default: 788 return mContractedChild; 789 } 790 } 791 792 private NotificationViewWrapper getVisibleWrapper(int visibleType) { 793 switch (visibleType) { 794 case VISIBLE_TYPE_EXPANDED: 795 return mExpandedWrapper; 796 case VISIBLE_TYPE_HEADSUP: 797 return mHeadsUpWrapper; 798 case VISIBLE_TYPE_CONTRACTED: 799 return mContractedWrapper; 800 default: 801 return null; 802 } 803 } 804 805 /** 806 * @return one of the static enum types in this view, calculated form the current state 807 */ 808 public int calculateVisibleType() { 809 if (mUserExpanding) { 810 int height = !mIsChildInGroup || isGroupExpanded() 811 || mContainingNotification.isExpanded(true /* allowOnKeyguard */) 812 ? mContainingNotification.getMaxContentHeight() 813 : mContainingNotification.getShowingLayout().getMinHeight(); 814 if (height == 0) { 815 height = mContentHeight; 816 } 817 int expandedVisualType = getVisualTypeForHeight(height); 818 int collapsedVisualType = mIsChildInGroup && !isGroupExpanded() 819 ? VISIBLE_TYPE_SINGLELINE 820 : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight()); 821 return mTransformationStartVisibleType == collapsedVisualType 822 ? expandedVisualType 823 : collapsedVisualType; 824 } 825 int intrinsicHeight = mContainingNotification.getIntrinsicHeight(); 826 int viewHeight = mContentHeight; 827 if (intrinsicHeight != 0) { 828 // the intrinsicHeight might be 0 because it was just reset. 829 viewHeight = Math.min(mContentHeight, intrinsicHeight); 830 } 831 return getVisualTypeForHeight(viewHeight); 832 } 833 834 private int getVisualTypeForHeight(float viewHeight) { 835 boolean noExpandedChild = mExpandedChild == null; 836 if (!noExpandedChild && viewHeight == mExpandedChild.getHeight()) { 837 return VISIBLE_TYPE_EXPANDED; 838 } 839 if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) { 840 return VISIBLE_TYPE_SINGLELINE; 841 } 842 843 if ((mIsHeadsUp || mHeadsupDisappearRunning) && mHeadsUpChild != null) { 844 if (viewHeight <= mHeadsUpChild.getHeight() || noExpandedChild) { 845 return VISIBLE_TYPE_HEADSUP; 846 } else { 847 return VISIBLE_TYPE_EXPANDED; 848 } 849 } else { 850 if (noExpandedChild || (viewHeight <= mContractedChild.getHeight() 851 && (!mIsChildInGroup || isGroupExpanded() 852 || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) { 853 return VISIBLE_TYPE_CONTRACTED; 854 } else { 855 return VISIBLE_TYPE_EXPANDED; 856 } 857 } 858 } 859 860 public boolean isContentExpandable() { 861 return mExpandedChild != null; 862 } 863 864 public void setDark(boolean dark, boolean fade, long delay) { 865 if (mContractedChild == null) { 866 return; 867 } 868 mDark = dark; 869 if (mVisibleType == VISIBLE_TYPE_CONTRACTED || !dark) { 870 mContractedWrapper.setDark(dark, fade, delay); 871 } 872 if (mVisibleType == VISIBLE_TYPE_EXPANDED || (mExpandedChild != null && !dark)) { 873 mExpandedWrapper.setDark(dark, fade, delay); 874 } 875 if (mVisibleType == VISIBLE_TYPE_HEADSUP || (mHeadsUpChild != null && !dark)) { 876 mHeadsUpWrapper.setDark(dark, fade, delay); 877 } 878 if (mSingleLineView != null && (mVisibleType == VISIBLE_TYPE_SINGLELINE || !dark)) { 879 mSingleLineView.setDark(dark, fade, delay); 880 } 881 } 882 883 public void setHeadsUp(boolean headsUp) { 884 mIsHeadsUp = headsUp; 885 selectLayout(false /* animate */, true /* force */); 886 updateExpandButtons(mExpandable); 887 } 888 889 @Override 890 public boolean hasOverlappingRendering() { 891 892 // This is not really true, but good enough when fading from the contracted to the expanded 893 // layout, and saves us some layers. 894 return false; 895 } 896 897 public void setShowingLegacyBackground(boolean showing) { 898 mShowingLegacyBackground = showing; 899 updateShowingLegacyBackground(); 900 } 901 902 private void updateShowingLegacyBackground() { 903 if (mContractedChild != null) { 904 mContractedWrapper.setShowingLegacyBackground(mShowingLegacyBackground); 905 } 906 if (mExpandedChild != null) { 907 mExpandedWrapper.setShowingLegacyBackground(mShowingLegacyBackground); 908 } 909 if (mHeadsUpChild != null) { 910 mHeadsUpWrapper.setShowingLegacyBackground(mShowingLegacyBackground); 911 } 912 } 913 914 public void setIsChildInGroup(boolean isChildInGroup) { 915 mIsChildInGroup = isChildInGroup; 916 updateSingleLineView(); 917 } 918 919 public void onNotificationUpdated(NotificationData.Entry entry) { 920 mStatusBarNotification = entry.notification; 921 mBeforeN = entry.targetSdk < Build.VERSION_CODES.N; 922 updateSingleLineView(); 923 applyRemoteInput(entry); 924 if (mContractedChild != null) { 925 mContractedWrapper.notifyContentUpdated(entry.notification); 926 } 927 if (mExpandedChild != null) { 928 mExpandedWrapper.notifyContentUpdated(entry.notification); 929 } 930 if (mHeadsUpChild != null) { 931 mHeadsUpWrapper.notifyContentUpdated(entry.notification); 932 } 933 updateShowingLegacyBackground(); 934 mForceSelectNextLayout = true; 935 setDark(mDark, false /* animate */, 0 /* delay */); 936 mPreviousExpandedRemoteInputIntent = null; 937 mPreviousHeadsUpRemoteInputIntent = null; 938 } 939 940 private void updateSingleLineView() { 941 if (mIsChildInGroup) { 942 mSingleLineView = mHybridGroupManager.bindFromNotification( 943 mSingleLineView, mStatusBarNotification.getNotification()); 944 } else if (mSingleLineView != null) { 945 removeView(mSingleLineView); 946 mSingleLineView = null; 947 } 948 } 949 950 private void applyRemoteInput(final NotificationData.Entry entry) { 951 if (mRemoteInputController == null) { 952 return; 953 } 954 955 boolean hasRemoteInput = false; 956 957 Notification.Action[] actions = entry.notification.getNotification().actions; 958 if (actions != null) { 959 for (Notification.Action a : actions) { 960 if (a.getRemoteInputs() != null) { 961 for (RemoteInput ri : a.getRemoteInputs()) { 962 if (ri.getAllowFreeFormInput()) { 963 hasRemoteInput = true; 964 break; 965 } 966 } 967 } 968 } 969 } 970 971 View bigContentView = mExpandedChild; 972 if (bigContentView != null) { 973 mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasRemoteInput, 974 mPreviousExpandedRemoteInputIntent, mCachedExpandedRemoteInput); 975 } else { 976 mExpandedRemoteInput = null; 977 } 978 if (mCachedExpandedRemoteInput != null 979 && mCachedExpandedRemoteInput != mExpandedRemoteInput) { 980 // We had a cached remote input but didn't reuse it. Clean up required. 981 mCachedExpandedRemoteInput.dispatchFinishTemporaryDetach(); 982 } 983 mCachedExpandedRemoteInput = null; 984 985 View headsUpContentView = mHeadsUpChild; 986 if (headsUpContentView != null) { 987 mHeadsUpRemoteInput = applyRemoteInput(headsUpContentView, entry, hasRemoteInput, 988 mPreviousHeadsUpRemoteInputIntent, mCachedHeadsUpRemoteInput); 989 } else { 990 mHeadsUpRemoteInput = null; 991 } 992 if (mCachedHeadsUpRemoteInput != null 993 && mCachedHeadsUpRemoteInput != mHeadsUpRemoteInput) { 994 // We had a cached remote input but didn't reuse it. Clean up required. 995 mCachedHeadsUpRemoteInput.dispatchFinishTemporaryDetach(); 996 } 997 mCachedHeadsUpRemoteInput = null; 998 } 999 1000 private RemoteInputView applyRemoteInput(View view, NotificationData.Entry entry, 1001 boolean hasRemoteInput, PendingIntent existingPendingIntent, 1002 RemoteInputView cachedView) { 1003 View actionContainerCandidate = view.findViewById( 1004 com.android.internal.R.id.actions_container); 1005 if (actionContainerCandidate instanceof FrameLayout) { 1006 RemoteInputView existing = (RemoteInputView) 1007 view.findViewWithTag(RemoteInputView.VIEW_TAG); 1008 1009 if (existing != null) { 1010 existing.onNotificationUpdateOrReset(); 1011 } 1012 1013 if (existing == null && hasRemoteInput) { 1014 ViewGroup actionContainer = (FrameLayout) actionContainerCandidate; 1015 if (cachedView == null) { 1016 RemoteInputView riv = RemoteInputView.inflate( 1017 mContext, actionContainer, entry, mRemoteInputController); 1018 1019 riv.setVisibility(View.INVISIBLE); 1020 actionContainer.addView(riv, new LayoutParams( 1021 ViewGroup.LayoutParams.MATCH_PARENT, 1022 ViewGroup.LayoutParams.MATCH_PARENT) 1023 ); 1024 existing = riv; 1025 } else { 1026 actionContainer.addView(cachedView); 1027 cachedView.dispatchFinishTemporaryDetach(); 1028 cachedView.requestFocus(); 1029 existing = cachedView; 1030 } 1031 } 1032 if (hasRemoteInput) { 1033 int color = entry.notification.getNotification().color; 1034 if (color == Notification.COLOR_DEFAULT) { 1035 color = mContext.getColor(R.color.default_remote_input_background); 1036 } 1037 existing.setBackgroundColor(NotificationColorUtil.ensureTextBackgroundColor(color, 1038 mContext.getColor(R.color.remote_input_text_enabled), 1039 mContext.getColor(R.color.remote_input_hint))); 1040 1041 if (existingPendingIntent != null || existing.isActive()) { 1042 // The current action could be gone, or the pending intent no longer valid. 1043 // If we find a matching action in the new notification, focus, otherwise close. 1044 Notification.Action[] actions = entry.notification.getNotification().actions; 1045 if (existingPendingIntent != null) { 1046 existing.setPendingIntent(existingPendingIntent); 1047 } 1048 if (existing.updatePendingIntentFromActions(actions)) { 1049 if (!existing.isActive()) { 1050 existing.focus(); 1051 } 1052 } else { 1053 if (existing.isActive()) { 1054 existing.close(); 1055 } 1056 } 1057 } 1058 } 1059 return existing; 1060 } 1061 return null; 1062 } 1063 1064 public void closeRemoteInput() { 1065 if (mHeadsUpRemoteInput != null) { 1066 mHeadsUpRemoteInput.close(); 1067 } 1068 if (mExpandedRemoteInput != null) { 1069 mExpandedRemoteInput.close(); 1070 } 1071 } 1072 1073 public void setGroupManager(NotificationGroupManager groupManager) { 1074 mGroupManager = groupManager; 1075 } 1076 1077 public void setRemoteInputController(RemoteInputController r) { 1078 mRemoteInputController = r; 1079 } 1080 1081 public void setExpandClickListener(OnClickListener expandClickListener) { 1082 mExpandClickListener = expandClickListener; 1083 } 1084 1085 public void updateExpandButtons(boolean expandable) { 1086 mExpandable = expandable; 1087 // if the expanded child has the same height as the collapsed one we hide it. 1088 if (mExpandedChild != null && mExpandedChild.getHeight() != 0) { 1089 if ((!mIsHeadsUp || mHeadsUpChild == null)) { 1090 if (mExpandedChild.getHeight() == mContractedChild.getHeight()) { 1091 expandable = false; 1092 } 1093 } else if (mExpandedChild.getHeight() == mHeadsUpChild.getHeight()) { 1094 expandable = false; 1095 } 1096 } 1097 if (mExpandedChild != null) { 1098 mExpandedWrapper.updateExpandability(expandable, mExpandClickListener); 1099 } 1100 if (mContractedChild != null) { 1101 mContractedWrapper.updateExpandability(expandable, mExpandClickListener); 1102 } 1103 if (mHeadsUpChild != null) { 1104 mHeadsUpWrapper.updateExpandability(expandable, mExpandClickListener); 1105 } 1106 } 1107 1108 public NotificationHeaderView getNotificationHeader() { 1109 NotificationHeaderView header = null; 1110 if (mContractedChild != null) { 1111 header = mContractedWrapper.getNotificationHeader(); 1112 } 1113 if (header == null && mExpandedChild != null) { 1114 header = mExpandedWrapper.getNotificationHeader(); 1115 } 1116 if (header == null && mHeadsUpChild != null) { 1117 header = mHeadsUpWrapper.getNotificationHeader(); 1118 } 1119 return header; 1120 } 1121 1122 public NotificationHeaderView getVisibleNotificationHeader() { 1123 NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); 1124 return wrapper == null ? null : wrapper.getNotificationHeader(); 1125 } 1126 1127 public void setContainingNotification(ExpandableNotificationRow containingNotification) { 1128 mContainingNotification = containingNotification; 1129 } 1130 1131 public void requestSelectLayout(boolean needsAnimation) { 1132 selectLayout(needsAnimation, false); 1133 } 1134 1135 public void reInflateViews() { 1136 if (mIsChildInGroup && mSingleLineView != null) { 1137 removeView(mSingleLineView); 1138 mSingleLineView = null; 1139 updateSingleLineView(); 1140 } 1141 } 1142 1143 public void setUserExpanding(boolean userExpanding) { 1144 mUserExpanding = userExpanding; 1145 if (userExpanding) { 1146 mTransformationStartVisibleType = mVisibleType; 1147 } else { 1148 mTransformationStartVisibleType = UNDEFINED; 1149 mVisibleType = calculateVisibleType(); 1150 updateViewVisibilities(mVisibleType); 1151 updateBackgroundColor(false); 1152 } 1153 } 1154 1155 /** 1156 * Set by how much the single line view should be indented. Used when a overflow indicator is 1157 * present and only during measuring 1158 */ 1159 public void setSingleLineWidthIndention(int singleLineWidthIndention) { 1160 if (singleLineWidthIndention != mSingleLineWidthIndention) { 1161 mSingleLineWidthIndention = singleLineWidthIndention; 1162 mContainingNotification.forceLayout(); 1163 forceLayout(); 1164 } 1165 } 1166 1167 public HybridNotificationView getSingleLineView() { 1168 return mSingleLineView; 1169 } 1170 1171 public void setRemoved() { 1172 if (mExpandedRemoteInput != null) { 1173 mExpandedRemoteInput.setRemoved(); 1174 } 1175 if (mHeadsUpRemoteInput != null) { 1176 mHeadsUpRemoteInput.setRemoved(); 1177 } 1178 } 1179 1180 public void setContentHeightAnimating(boolean animating) { 1181 if (!animating) { 1182 mContentHeightAtAnimationStart = UNDEFINED; 1183 } 1184 } 1185 1186 public void setHeadsupDisappearRunning(boolean headsupDisappearRunning) { 1187 mHeadsupDisappearRunning = headsupDisappearRunning; 1188 selectLayout(false /* animate */, true /* force */); 1189 } 1190 1191 public void setFocusOnVisibilityChange() { 1192 mFocusOnVisibilityChange = true; 1193 } 1194 } 1195