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.notification.row; 18 19 import android.annotation.Nullable; 20 import android.app.Notification; 21 import android.app.PendingIntent; 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.ArrayMap; 27 import android.util.ArraySet; 28 import android.util.AttributeSet; 29 import android.util.Log; 30 import android.view.MotionEvent; 31 import android.view.NotificationHeaderView; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.ViewTreeObserver; 35 import android.widget.FrameLayout; 36 import android.widget.ImageView; 37 import android.widget.LinearLayout; 38 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.util.ContrastColorUtil; 41 import com.android.systemui.Dependency; 42 import com.android.systemui.R; 43 import com.android.systemui.statusbar.MediaTransferManager; 44 import com.android.systemui.statusbar.RemoteInputController; 45 import com.android.systemui.statusbar.SmartReplyController; 46 import com.android.systemui.statusbar.TransformableView; 47 import com.android.systemui.statusbar.notification.NotificationUtils; 48 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 49 import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper; 50 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 51 import com.android.systemui.statusbar.phone.NotificationGroupManager; 52 import com.android.systemui.statusbar.policy.InflatedSmartReplies; 53 import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions; 54 import com.android.systemui.statusbar.policy.RemoteInputView; 55 import com.android.systemui.statusbar.policy.SmartReplyConstants; 56 import com.android.systemui.statusbar.policy.SmartReplyView; 57 58 import java.io.FileDescriptor; 59 import java.io.PrintWriter; 60 61 /** 62 * A frame layout containing the actual payload of the notification, including the contracted, 63 * expanded and heads up layout. This class is responsible for clipping the content and and 64 * switching between the expanded, contracted and the heads up view depending on its clipped size. 65 */ 66 public class NotificationContentView extends FrameLayout { 67 68 private static final String TAG = "NotificationContentView"; 69 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 70 public static final int VISIBLE_TYPE_CONTRACTED = 0; 71 public static final int VISIBLE_TYPE_EXPANDED = 1; 72 public static final int VISIBLE_TYPE_HEADSUP = 2; 73 private static final int VISIBLE_TYPE_SINGLELINE = 3; 74 public static final int VISIBLE_TYPE_AMBIENT = 4; 75 public static final int UNDEFINED = -1; 76 77 private final Rect mClipBounds = new Rect(); 78 79 private int mMinContractedHeight; 80 private int mNotificationContentMarginEnd; 81 private View mContractedChild; 82 private View mExpandedChild; 83 private View mHeadsUpChild; 84 private HybridNotificationView mSingleLineView; 85 private View mAmbientChild; 86 87 private RemoteInputView mExpandedRemoteInput; 88 private RemoteInputView mHeadsUpRemoteInput; 89 90 private SmartReplyConstants mSmartReplyConstants; 91 private SmartReplyView mExpandedSmartReplyView; 92 private SmartReplyView mHeadsUpSmartReplyView; 93 private SmartReplyController mSmartReplyController; 94 private InflatedSmartReplies mExpandedInflatedSmartReplies; 95 private InflatedSmartReplies mHeadsUpInflatedSmartReplies; 96 private SmartRepliesAndActions mCurrentSmartRepliesAndActions; 97 98 private NotificationViewWrapper mContractedWrapper; 99 private NotificationViewWrapper mExpandedWrapper; 100 private NotificationViewWrapper mHeadsUpWrapper; 101 private NotificationViewWrapper mAmbientWrapper; 102 private HybridGroupManager mHybridGroupManager; 103 private int mClipTopAmount; 104 private int mContentHeight; 105 private int mVisibleType = VISIBLE_TYPE_CONTRACTED; 106 private boolean mDark; 107 private boolean mAnimate; 108 private boolean mIsHeadsUp; 109 private boolean mLegacy; 110 private boolean mIsChildInGroup; 111 private int mSmallHeight; 112 private int mHeadsUpHeight; 113 private int mNotificationMaxHeight; 114 private int mNotificationAmbientHeight; 115 private StatusBarNotification mStatusBarNotification; 116 private NotificationGroupManager mGroupManager; 117 private RemoteInputController mRemoteInputController; 118 private Runnable mExpandedVisibleListener; 119 /** 120 * List of listeners for when content views become inactive (i.e. not the showing view). 121 */ 122 private final ArrayMap<View, Runnable> mOnContentViewInactiveListeners = new ArrayMap<>(); 123 124 private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener 125 = new ViewTreeObserver.OnPreDrawListener() { 126 @Override 127 public boolean onPreDraw() { 128 // We need to post since we don't want the notification to animate on the very first 129 // frame 130 post(new Runnable() { 131 @Override 132 public void run() { 133 mAnimate = true; 134 } 135 }); 136 getViewTreeObserver().removeOnPreDrawListener(this); 137 return true; 138 } 139 }; 140 141 private OnClickListener mExpandClickListener; 142 private boolean mBeforeN; 143 private boolean mExpandable; 144 private boolean mClipToActualHeight = true; 145 private ExpandableNotificationRow mContainingNotification; 146 /** The visible type at the start of a touch driven transformation */ 147 private int mTransformationStartVisibleType; 148 /** The visible type at the start of an animation driven transformation */ 149 private int mAnimationStartVisibleType = UNDEFINED; 150 private boolean mUserExpanding; 151 private int mSingleLineWidthIndention; 152 private boolean mForceSelectNextLayout = true; 153 private PendingIntent mPreviousExpandedRemoteInputIntent; 154 private PendingIntent mPreviousHeadsUpRemoteInputIntent; 155 private RemoteInputView mCachedExpandedRemoteInput; 156 private RemoteInputView mCachedHeadsUpRemoteInput; 157 158 private int mContentHeightAtAnimationStart = UNDEFINED; 159 private boolean mFocusOnVisibilityChange; 160 private boolean mHeadsUpAnimatingAway; 161 private boolean mIconsVisible; 162 private int mClipBottomAmount; 163 private boolean mIsLowPriority; 164 private boolean mIsContentExpandable; 165 private boolean mRemoteInputVisible; 166 private int mUnrestrictedContentHeight; 167 private MediaTransferManager mMediaTransferManager; 168 169 NotificationContentView(Context context, AttributeSet attrs)170 public NotificationContentView(Context context, AttributeSet attrs) { 171 super(context, attrs); 172 mHybridGroupManager = new HybridGroupManager(getContext(), this); 173 mMediaTransferManager = new MediaTransferManager(getContext()); 174 mSmartReplyConstants = Dependency.get(SmartReplyConstants.class); 175 mSmartReplyController = Dependency.get(SmartReplyController.class); 176 initView(); 177 } 178 initView()179 public void initView() { 180 mMinContractedHeight = getResources().getDimensionPixelSize( 181 R.dimen.min_notification_layout_height); 182 mNotificationContentMarginEnd = getResources().getDimensionPixelSize( 183 com.android.internal.R.dimen.notification_content_margin_end); 184 } 185 setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight, int ambientHeight)186 public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight, 187 int ambientHeight) { 188 mSmallHeight = smallHeight; 189 mHeadsUpHeight = headsUpMaxHeight; 190 mNotificationMaxHeight = maxHeight; 191 mNotificationAmbientHeight = ambientHeight; 192 } 193 194 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)195 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 196 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 197 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 198 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 199 int maxSize = Integer.MAX_VALUE / 2; 200 int width = MeasureSpec.getSize(widthMeasureSpec); 201 if (hasFixedHeight || isHeightLimited) { 202 maxSize = MeasureSpec.getSize(heightMeasureSpec); 203 } 204 int maxChildHeight = 0; 205 if (mExpandedChild != null) { 206 int notificationMaxHeight = mNotificationMaxHeight; 207 if (mExpandedSmartReplyView != null) { 208 notificationMaxHeight += mExpandedSmartReplyView.getHeightUpperLimit(); 209 } 210 notificationMaxHeight += mExpandedWrapper.getExtraMeasureHeight(); 211 int size = notificationMaxHeight; 212 ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams(); 213 boolean useExactly = false; 214 if (layoutParams.height >= 0) { 215 // An actual height is set 216 size = Math.min(size, layoutParams.height); 217 useExactly = true; 218 } 219 int spec = MeasureSpec.makeMeasureSpec(size, useExactly 220 ? MeasureSpec.EXACTLY 221 : MeasureSpec.AT_MOST); 222 measureChildWithMargins(mExpandedChild, widthMeasureSpec, 0, spec, 0); 223 maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight()); 224 } 225 if (mContractedChild != null) { 226 int heightSpec; 227 int size = mSmallHeight; 228 ViewGroup.LayoutParams layoutParams = mContractedChild.getLayoutParams(); 229 boolean useExactly = false; 230 if (layoutParams.height >= 0) { 231 // An actual height is set 232 size = Math.min(size, layoutParams.height); 233 useExactly = true; 234 } 235 if (shouldContractedBeFixedSize() || useExactly) { 236 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 237 } else { 238 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); 239 } 240 measureChildWithMargins(mContractedChild, widthMeasureSpec, 0, heightSpec, 0); 241 int measuredHeight = mContractedChild.getMeasuredHeight(); 242 if (measuredHeight < mMinContractedHeight) { 243 heightSpec = MeasureSpec.makeMeasureSpec(mMinContractedHeight, MeasureSpec.EXACTLY); 244 measureChildWithMargins(mContractedChild, widthMeasureSpec, 0, heightSpec, 0); 245 } 246 maxChildHeight = Math.max(maxChildHeight, measuredHeight); 247 if (updateContractedHeaderWidth()) { 248 measureChildWithMargins(mContractedChild, widthMeasureSpec, 0, heightSpec, 0); 249 } 250 if (mExpandedChild != null 251 && mContractedChild.getMeasuredHeight() > mExpandedChild.getMeasuredHeight()) { 252 // the Expanded child is smaller then the collapsed. Let's remeasure it. 253 heightSpec = MeasureSpec.makeMeasureSpec(mContractedChild.getMeasuredHeight(), 254 MeasureSpec.EXACTLY); 255 measureChildWithMargins(mExpandedChild, widthMeasureSpec, 0, heightSpec, 0); 256 } 257 } 258 if (mHeadsUpChild != null) { 259 int maxHeight = mHeadsUpHeight; 260 if (mHeadsUpSmartReplyView != null) { 261 maxHeight += mHeadsUpSmartReplyView.getHeightUpperLimit(); 262 } 263 maxHeight += mHeadsUpWrapper.getExtraMeasureHeight(); 264 int size = maxHeight; 265 ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams(); 266 boolean useExactly = false; 267 if (layoutParams.height >= 0) { 268 // An actual height is set 269 size = Math.min(size, layoutParams.height); 270 useExactly = true; 271 } 272 measureChildWithMargins(mHeadsUpChild, widthMeasureSpec, 0, 273 MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY 274 : MeasureSpec.AT_MOST), 0); 275 maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight()); 276 } 277 if (mSingleLineView != null) { 278 int singleLineWidthSpec = widthMeasureSpec; 279 if (mSingleLineWidthIndention != 0 280 && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { 281 singleLineWidthSpec = MeasureSpec.makeMeasureSpec( 282 width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(), 283 MeasureSpec.EXACTLY); 284 } 285 mSingleLineView.measure(singleLineWidthSpec, 286 MeasureSpec.makeMeasureSpec(mNotificationMaxHeight, MeasureSpec.AT_MOST)); 287 maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight()); 288 } 289 if (mAmbientChild != null) { 290 int size = mNotificationAmbientHeight; 291 ViewGroup.LayoutParams layoutParams = mAmbientChild.getLayoutParams(); 292 boolean useExactly = false; 293 if (layoutParams.height >= 0) { 294 // An actual height is set 295 size = Math.min(size, layoutParams.height); 296 useExactly = true; 297 } 298 mAmbientChild.measure(widthMeasureSpec, 299 MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY 300 : MeasureSpec.AT_MOST)); 301 maxChildHeight = Math.max(maxChildHeight, mAmbientChild.getMeasuredHeight()); 302 } 303 int ownHeight = Math.min(maxChildHeight, maxSize); 304 setMeasuredDimension(width, ownHeight); 305 } 306 307 /** 308 * Get the extra height that needs to be added to the notification height for a given 309 * {@link RemoteInputView}. 310 * This is needed when the user is inline replying in order to ensure that the reply bar has 311 * enough padding. 312 * 313 * @param remoteInput The remote input to check. 314 * @return The extra height needed. 315 */ getExtraRemoteInputHeight(RemoteInputView remoteInput)316 private int getExtraRemoteInputHeight(RemoteInputView remoteInput) { 317 if (remoteInput != null && (remoteInput.isActive() || remoteInput.isSending())) { 318 return getResources().getDimensionPixelSize( 319 com.android.internal.R.dimen.notification_content_margin); 320 } 321 return 0; 322 } 323 updateContractedHeaderWidth()324 private boolean updateContractedHeaderWidth() { 325 // We need to update the expanded and the collapsed header to have exactly the same with to 326 // have the expand buttons laid out at the same location. 327 NotificationHeaderView contractedHeader = mContractedWrapper.getNotificationHeader(); 328 if (contractedHeader != null) { 329 if (mExpandedChild != null 330 && mExpandedWrapper.getNotificationHeader() != null) { 331 NotificationHeaderView expandedHeader = mExpandedWrapper.getNotificationHeader(); 332 333 int headerTextMargin = expandedHeader.getHeaderTextMarginEnd(); 334 if (headerTextMargin != contractedHeader.getHeaderTextMarginEnd()) { 335 contractedHeader.setHeaderTextMarginEnd(headerTextMargin); 336 return true; 337 } 338 } else { 339 int paddingEnd = mNotificationContentMarginEnd; 340 if (contractedHeader.getPaddingEnd() != paddingEnd) { 341 contractedHeader.setPadding( 342 contractedHeader.isLayoutRtl() 343 ? paddingEnd 344 : contractedHeader.getPaddingLeft(), 345 contractedHeader.getPaddingTop(), 346 contractedHeader.isLayoutRtl() 347 ? contractedHeader.getPaddingLeft() 348 : paddingEnd, 349 contractedHeader.getPaddingBottom()); 350 contractedHeader.setShowWorkBadgeAtEnd(false); 351 return true; 352 } 353 } 354 } 355 return false; 356 } 357 shouldContractedBeFixedSize()358 private boolean shouldContractedBeFixedSize() { 359 return mBeforeN && mContractedWrapper instanceof NotificationCustomViewWrapper; 360 } 361 362 @Override onLayout(boolean changed, int left, int top, int right, int bottom)363 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 364 int previousHeight = 0; 365 if (mExpandedChild != null) { 366 previousHeight = mExpandedChild.getHeight(); 367 } 368 super.onLayout(changed, left, top, right, bottom); 369 if (previousHeight != 0 && mExpandedChild.getHeight() != previousHeight) { 370 mContentHeightAtAnimationStart = previousHeight; 371 } 372 updateClipping(); 373 invalidateOutline(); 374 selectLayout(false /* animate */, mForceSelectNextLayout /* force */); 375 mForceSelectNextLayout = false; 376 updateExpandButtons(mExpandable); 377 } 378 379 @Override onAttachedToWindow()380 protected void onAttachedToWindow() { 381 super.onAttachedToWindow(); 382 updateVisibility(); 383 } 384 getContractedChild()385 public View getContractedChild() { 386 return mContractedChild; 387 } 388 getExpandedChild()389 public View getExpandedChild() { 390 return mExpandedChild; 391 } 392 getHeadsUpChild()393 public View getHeadsUpChild() { 394 return mHeadsUpChild; 395 } 396 getAmbientChild()397 public View getAmbientChild() { 398 return mAmbientChild; 399 } 400 401 /** 402 * Sets the contracted view. Child may be null to remove the content view. 403 * 404 * @param child contracted content view to set 405 */ setContractedChild(@ullable View child)406 public void setContractedChild(@Nullable View child) { 407 if (mContractedChild != null) { 408 mContractedChild.animate().cancel(); 409 removeView(mContractedChild); 410 } 411 if (child == null) { 412 mContractedChild = null; 413 mContractedWrapper = null; 414 if (mTransformationStartVisibleType == VISIBLE_TYPE_CONTRACTED) { 415 mTransformationStartVisibleType = UNDEFINED; 416 } 417 return; 418 } 419 addView(child); 420 mContractedChild = child; 421 mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child, 422 mContainingNotification); 423 } 424 getWrapperForView(View child)425 private NotificationViewWrapper getWrapperForView(View child) { 426 if (child == mContractedChild) { 427 return mContractedWrapper; 428 } 429 if (child == mExpandedChild) { 430 return mExpandedWrapper; 431 } 432 if (child == mHeadsUpChild) { 433 return mHeadsUpWrapper; 434 } 435 if (child == mAmbientChild) { 436 return mAmbientWrapper; 437 } 438 return null; 439 } 440 441 /** 442 * Sets the expanded view. Child may be null to remove the content view. 443 * 444 * @param child expanded content view to set 445 */ setExpandedChild(@ullable View child)446 public void setExpandedChild(@Nullable View child) { 447 if (mExpandedChild != null) { 448 mPreviousExpandedRemoteInputIntent = null; 449 if (mExpandedRemoteInput != null) { 450 mExpandedRemoteInput.onNotificationUpdateOrReset(); 451 if (mExpandedRemoteInput.isActive()) { 452 mPreviousExpandedRemoteInputIntent = mExpandedRemoteInput.getPendingIntent(); 453 mCachedExpandedRemoteInput = mExpandedRemoteInput; 454 mExpandedRemoteInput.dispatchStartTemporaryDetach(); 455 ((ViewGroup)mExpandedRemoteInput.getParent()).removeView(mExpandedRemoteInput); 456 } 457 } 458 mExpandedChild.animate().cancel(); 459 removeView(mExpandedChild); 460 mExpandedRemoteInput = null; 461 } 462 if (child == null) { 463 mExpandedChild = null; 464 mExpandedWrapper = null; 465 if (mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED) { 466 mTransformationStartVisibleType = UNDEFINED; 467 } 468 if (mVisibleType == VISIBLE_TYPE_EXPANDED) { 469 selectLayout(false /* animate */, true /* force */); 470 } 471 return; 472 } 473 addView(child); 474 mExpandedChild = child; 475 mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child, 476 mContainingNotification); 477 } 478 479 /** 480 * Sets the heads up view. Child may be null to remove the content view. 481 * 482 * @param child heads up content view to set 483 */ setHeadsUpChild(@ullable View child)484 public void setHeadsUpChild(@Nullable View child) { 485 if (mHeadsUpChild != null) { 486 mPreviousHeadsUpRemoteInputIntent = null; 487 if (mHeadsUpRemoteInput != null) { 488 mHeadsUpRemoteInput.onNotificationUpdateOrReset(); 489 if (mHeadsUpRemoteInput.isActive()) { 490 mPreviousHeadsUpRemoteInputIntent = mHeadsUpRemoteInput.getPendingIntent(); 491 mCachedHeadsUpRemoteInput = mHeadsUpRemoteInput; 492 mHeadsUpRemoteInput.dispatchStartTemporaryDetach(); 493 ((ViewGroup)mHeadsUpRemoteInput.getParent()).removeView(mHeadsUpRemoteInput); 494 } 495 } 496 mHeadsUpChild.animate().cancel(); 497 removeView(mHeadsUpChild); 498 mHeadsUpRemoteInput = null; 499 } 500 if (child == null) { 501 mHeadsUpChild = null; 502 mHeadsUpWrapper = null; 503 if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) { 504 mTransformationStartVisibleType = UNDEFINED; 505 } 506 if (mVisibleType == VISIBLE_TYPE_HEADSUP) { 507 selectLayout(false /* animate */, true /* force */); 508 } 509 return; 510 } 511 addView(child); 512 mHeadsUpChild = child; 513 mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, 514 mContainingNotification); 515 } 516 517 /** 518 * Sets the ambient view. Child may be null to remove the content view. 519 * 520 * @param child ambient content view to set 521 */ setAmbientChild(@ullable View child)522 public void setAmbientChild(@Nullable View child) { 523 if (mAmbientChild != null) { 524 mAmbientChild.animate().cancel(); 525 removeView(mAmbientChild); 526 } 527 if (child == null) { 528 mAmbientChild = null; 529 mAmbientWrapper = null; 530 if (mTransformationStartVisibleType == VISIBLE_TYPE_AMBIENT) { 531 mTransformationStartVisibleType = UNDEFINED; 532 } 533 if (mVisibleType == VISIBLE_TYPE_AMBIENT) { 534 selectLayout(false /* animate */, true /* force */); 535 } 536 return; 537 } 538 addView(child); 539 mAmbientChild = child; 540 mAmbientWrapper = NotificationViewWrapper.wrap(getContext(), child, 541 mContainingNotification); 542 } 543 544 @Override onViewAdded(View child)545 public void onViewAdded(View child) { 546 super.onViewAdded(child); 547 child.setTag(R.id.row_tag_for_content_view, mContainingNotification); 548 } 549 550 @Override onVisibilityChanged(View changedView, int visibility)551 protected void onVisibilityChanged(View changedView, int visibility) { 552 super.onVisibilityChanged(changedView, visibility); 553 updateVisibility(); 554 if (visibility != VISIBLE) { 555 // View is no longer visible so all content views are inactive. 556 for (Runnable r : mOnContentViewInactiveListeners.values()) { 557 r.run(); 558 } 559 mOnContentViewInactiveListeners.clear(); 560 } 561 } 562 updateVisibility()563 private void updateVisibility() { 564 setVisible(isShown()); 565 } 566 567 @Override onDetachedFromWindow()568 protected void onDetachedFromWindow() { 569 super.onDetachedFromWindow(); 570 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 571 } 572 setVisible(final boolean isVisible)573 private void setVisible(final boolean isVisible) { 574 if (isVisible) { 575 // This call can happen multiple times, but removing only removes a single one. 576 // We therefore need to remove the old one. 577 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 578 // We only animate if we are drawn at least once, otherwise the view might animate when 579 // it's shown the first time 580 getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener); 581 } else { 582 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 583 mAnimate = false; 584 } 585 } 586 focusExpandButtonIfNecessary()587 private void focusExpandButtonIfNecessary() { 588 if (mFocusOnVisibilityChange) { 589 NotificationHeaderView header = getVisibleNotificationHeader(); 590 if (header != null) { 591 ImageView expandButton = header.getExpandButton(); 592 if (expandButton != null) { 593 expandButton.requestAccessibilityFocus(); 594 } 595 } 596 mFocusOnVisibilityChange = false; 597 } 598 } 599 setContentHeight(int contentHeight)600 public void setContentHeight(int contentHeight) { 601 mUnrestrictedContentHeight = Math.max(contentHeight, getMinHeight()); 602 int maxContentHeight = mContainingNotification.getIntrinsicHeight() 603 - getExtraRemoteInputHeight(mExpandedRemoteInput) 604 - getExtraRemoteInputHeight(mHeadsUpRemoteInput); 605 mContentHeight = Math.min(mUnrestrictedContentHeight, maxContentHeight); 606 selectLayout(mAnimate /* animate */, false /* force */); 607 608 if (mContractedChild == null) { 609 // Contracted child may be null if this is the public content view and we don't need to 610 // show it. 611 return; 612 } 613 614 int minHeightHint = getMinContentHeightHint(); 615 616 NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); 617 if (wrapper != null) { 618 wrapper.setContentHeight(mUnrestrictedContentHeight, minHeightHint); 619 } 620 621 wrapper = getVisibleWrapper(mTransformationStartVisibleType); 622 if (wrapper != null) { 623 wrapper.setContentHeight(mUnrestrictedContentHeight, minHeightHint); 624 } 625 626 updateClipping(); 627 invalidateOutline(); 628 } 629 630 /** 631 * @return the minimum apparent height that the wrapper should allow for the purpose 632 * of aligning elements at the bottom edge. If this is larger than the content 633 * height, the notification is clipped instead of being further shrunk. 634 */ getMinContentHeightHint()635 private int getMinContentHeightHint() { 636 if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) { 637 return mContext.getResources().getDimensionPixelSize( 638 com.android.internal.R.dimen.notification_action_list_height); 639 } 640 641 // Transition between heads-up & expanded, or pinned. 642 if (mHeadsUpChild != null && mExpandedChild != null) { 643 boolean transitioningBetweenHunAndExpanded = 644 isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) || 645 isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP); 646 boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED) 647 && (mIsHeadsUp || mHeadsUpAnimatingAway) 648 && !mContainingNotification.isOnKeyguard(); 649 if (transitioningBetweenHunAndExpanded || pinned) { 650 return Math.min(getViewHeight(VISIBLE_TYPE_HEADSUP), 651 getViewHeight(VISIBLE_TYPE_EXPANDED)); 652 } 653 } 654 655 // Size change of the expanded version 656 if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart >= 0 657 && mExpandedChild != null) { 658 return Math.min(mContentHeightAtAnimationStart, getViewHeight(VISIBLE_TYPE_EXPANDED)); 659 } 660 661 int hint; 662 if (mAmbientChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_AMBIENT)) { 663 hint = mAmbientChild.getHeight(); 664 } else if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) { 665 hint = getViewHeight(VISIBLE_TYPE_HEADSUP); 666 } else if (mExpandedChild != null) { 667 hint = getViewHeight(VISIBLE_TYPE_EXPANDED); 668 } else { 669 hint = getViewHeight(VISIBLE_TYPE_CONTRACTED) 670 + mContext.getResources().getDimensionPixelSize( 671 com.android.internal.R.dimen.notification_action_list_height); 672 } 673 674 if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) { 675 hint = Math.min(hint, getViewHeight(VISIBLE_TYPE_EXPANDED)); 676 } 677 return hint; 678 } 679 isTransitioningFromTo(int from, int to)680 private boolean isTransitioningFromTo(int from, int to) { 681 return (mTransformationStartVisibleType == from || mAnimationStartVisibleType == from) 682 && mVisibleType == to; 683 } 684 isVisibleOrTransitioning(int type)685 private boolean isVisibleOrTransitioning(int type) { 686 return mVisibleType == type || mTransformationStartVisibleType == type 687 || mAnimationStartVisibleType == type; 688 } 689 updateContentTransformation()690 private void updateContentTransformation() { 691 int visibleType = calculateVisibleType(); 692 if (visibleType != mVisibleType) { 693 // A new transformation starts 694 mTransformationStartVisibleType = mVisibleType; 695 final TransformableView shownView = getTransformableViewForVisibleType(visibleType); 696 final TransformableView hiddenView = getTransformableViewForVisibleType( 697 mTransformationStartVisibleType); 698 shownView.transformFrom(hiddenView, 0.0f); 699 getViewForVisibleType(visibleType).setVisibility(View.VISIBLE); 700 hiddenView.transformTo(shownView, 0.0f); 701 mVisibleType = visibleType; 702 updateBackgroundColor(true /* animate */); 703 } 704 if (mForceSelectNextLayout) { 705 forceUpdateVisibilities(); 706 } 707 if (mTransformationStartVisibleType != UNDEFINED 708 && mVisibleType != mTransformationStartVisibleType 709 && getViewForVisibleType(mTransformationStartVisibleType) != null) { 710 final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType); 711 final TransformableView hiddenView = getTransformableViewForVisibleType( 712 mTransformationStartVisibleType); 713 float transformationAmount = calculateTransformationAmount(); 714 shownView.transformFrom(hiddenView, transformationAmount); 715 hiddenView.transformTo(shownView, transformationAmount); 716 updateBackgroundTransformation(transformationAmount); 717 } else { 718 updateViewVisibilities(visibleType); 719 updateBackgroundColor(false); 720 } 721 } 722 updateBackgroundTransformation(float transformationAmount)723 private void updateBackgroundTransformation(float transformationAmount) { 724 int endColor = getBackgroundColor(mVisibleType); 725 int startColor = getBackgroundColor(mTransformationStartVisibleType); 726 if (endColor != startColor) { 727 if (startColor == 0) { 728 startColor = mContainingNotification.getBackgroundColorWithoutTint(); 729 } 730 if (endColor == 0) { 731 endColor = mContainingNotification.getBackgroundColorWithoutTint(); 732 } 733 endColor = NotificationUtils.interpolateColors(startColor, endColor, 734 transformationAmount); 735 } 736 mContainingNotification.updateBackgroundAlpha(transformationAmount); 737 mContainingNotification.setContentBackground(endColor, false, this); 738 } 739 calculateTransformationAmount()740 private float calculateTransformationAmount() { 741 int startHeight = getViewHeight(mTransformationStartVisibleType); 742 int endHeight = getViewHeight(mVisibleType); 743 int progress = Math.abs(mContentHeight - startHeight); 744 int totalDistance = Math.abs(endHeight - startHeight); 745 if (totalDistance == 0) { 746 Log.wtf(TAG, "the total transformation distance is 0" 747 + "\n StartType: " + mTransformationStartVisibleType + " height: " + startHeight 748 + "\n VisibleType: " + mVisibleType + " height: " + endHeight 749 + "\n mContentHeight: " + mContentHeight); 750 return 1.0f; 751 } 752 float amount = (float) progress / (float) totalDistance; 753 return Math.min(1.0f, amount); 754 } 755 getContentHeight()756 public int getContentHeight() { 757 return mContentHeight; 758 } 759 getMaxHeight()760 public int getMaxHeight() { 761 if (mExpandedChild != null) { 762 return getViewHeight(VISIBLE_TYPE_EXPANDED) 763 + getExtraRemoteInputHeight(mExpandedRemoteInput); 764 } else if (mContainingNotification.isOnAmbient() && getShowingAmbientView() != null) { 765 return getShowingAmbientView().getHeight(); 766 } else if (mIsHeadsUp && mHeadsUpChild != null && !mContainingNotification.isOnKeyguard()) { 767 return getViewHeight(VISIBLE_TYPE_HEADSUP) 768 + getExtraRemoteInputHeight(mHeadsUpRemoteInput); 769 } else if (mContractedChild != null) { 770 return getViewHeight(VISIBLE_TYPE_CONTRACTED); 771 } 772 return mNotificationMaxHeight; 773 } 774 getViewHeight(int visibleType)775 private int getViewHeight(int visibleType) { 776 View view = getViewForVisibleType(visibleType); 777 int height = view.getHeight(); 778 NotificationViewWrapper viewWrapper = getWrapperForView(view); 779 if (viewWrapper != null) { 780 height += viewWrapper.getHeaderTranslation(); 781 } 782 return height; 783 } 784 getMinHeight()785 public int getMinHeight() { 786 return getMinHeight(false /* likeGroupExpanded */); 787 } 788 getMinHeight(boolean likeGroupExpanded)789 public int getMinHeight(boolean likeGroupExpanded) { 790 if (mContainingNotification.isOnAmbient() && getShowingAmbientView() != null) { 791 return getShowingAmbientView().getHeight(); 792 } else if (likeGroupExpanded || !mIsChildInGroup || isGroupExpanded()) { 793 return mContractedChild != null 794 ? getViewHeight(VISIBLE_TYPE_CONTRACTED) : mMinContractedHeight; 795 } else { 796 return mSingleLineView.getHeight(); 797 } 798 } 799 getShowingAmbientView()800 public View getShowingAmbientView() { 801 View v = mIsChildInGroup ? mSingleLineView : mAmbientChild; 802 if (v != null) { 803 return v; 804 } else { 805 return mContractedChild; 806 } 807 } 808 isGroupExpanded()809 private boolean isGroupExpanded() { 810 return mGroupManager.isGroupExpanded(mStatusBarNotification); 811 } 812 setClipTopAmount(int clipTopAmount)813 public void setClipTopAmount(int clipTopAmount) { 814 mClipTopAmount = clipTopAmount; 815 updateClipping(); 816 } 817 818 setClipBottomAmount(int clipBottomAmount)819 public void setClipBottomAmount(int clipBottomAmount) { 820 mClipBottomAmount = clipBottomAmount; 821 updateClipping(); 822 } 823 824 @Override setTranslationY(float translationY)825 public void setTranslationY(float translationY) { 826 super.setTranslationY(translationY); 827 updateClipping(); 828 } 829 updateClipping()830 private void updateClipping() { 831 if (mClipToActualHeight) { 832 int top = (int) (mClipTopAmount - getTranslationY()); 833 int bottom = (int) (mUnrestrictedContentHeight - mClipBottomAmount - getTranslationY()); 834 bottom = Math.max(top, bottom); 835 mClipBounds.set(0, top, getWidth(), bottom); 836 setClipBounds(mClipBounds); 837 } else { 838 setClipBounds(null); 839 } 840 } 841 setClipToActualHeight(boolean clipToActualHeight)842 public void setClipToActualHeight(boolean clipToActualHeight) { 843 mClipToActualHeight = clipToActualHeight; 844 updateClipping(); 845 } 846 selectLayout(boolean animate, boolean force)847 private void selectLayout(boolean animate, boolean force) { 848 if (mContractedChild == null) { 849 return; 850 } 851 if (mUserExpanding) { 852 updateContentTransformation(); 853 } else { 854 int visibleType = calculateVisibleType(); 855 boolean changedType = visibleType != mVisibleType; 856 if (changedType || force) { 857 View visibleView = getViewForVisibleType(visibleType); 858 if (visibleView != null) { 859 visibleView.setVisibility(VISIBLE); 860 transferRemoteInputFocus(visibleType); 861 } 862 863 if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null) 864 || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null) 865 || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null) 866 || visibleType == VISIBLE_TYPE_CONTRACTED)) { 867 animateToVisibleType(visibleType); 868 } else { 869 updateViewVisibilities(visibleType); 870 } 871 mVisibleType = visibleType; 872 if (changedType) { 873 focusExpandButtonIfNecessary(); 874 } 875 NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType); 876 if (visibleWrapper != null) { 877 visibleWrapper.setContentHeight(mUnrestrictedContentHeight, 878 getMinContentHeightHint()); 879 } 880 updateBackgroundColor(animate); 881 } 882 } 883 } 884 forceUpdateVisibilities()885 private void forceUpdateVisibilities() { 886 forceUpdateVisibility(VISIBLE_TYPE_CONTRACTED, mContractedChild, mContractedWrapper); 887 forceUpdateVisibility(VISIBLE_TYPE_EXPANDED, mExpandedChild, mExpandedWrapper); 888 forceUpdateVisibility(VISIBLE_TYPE_HEADSUP, mHeadsUpChild, mHeadsUpWrapper); 889 forceUpdateVisibility(VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView); 890 forceUpdateVisibility(VISIBLE_TYPE_AMBIENT, mAmbientChild, mAmbientWrapper); 891 fireExpandedVisibleListenerIfVisible(); 892 // forceUpdateVisibilities cancels outstanding animations without updating the 893 // mAnimationStartVisibleType. Do so here instead. 894 mAnimationStartVisibleType = UNDEFINED; 895 } 896 fireExpandedVisibleListenerIfVisible()897 private void fireExpandedVisibleListenerIfVisible() { 898 if (mExpandedVisibleListener != null && mExpandedChild != null && isShown() 899 && mExpandedChild.getVisibility() == VISIBLE) { 900 Runnable listener = mExpandedVisibleListener; 901 mExpandedVisibleListener = null; 902 listener.run(); 903 } 904 } 905 forceUpdateVisibility(int type, View view, TransformableView wrapper)906 private void forceUpdateVisibility(int type, View view, TransformableView wrapper) { 907 if (view == null) { 908 return; 909 } 910 boolean visible = mVisibleType == type 911 || mTransformationStartVisibleType == type; 912 if (!visible) { 913 view.setVisibility(INVISIBLE); 914 } else { 915 wrapper.setVisible(true); 916 } 917 } 918 updateBackgroundColor(boolean animate)919 public void updateBackgroundColor(boolean animate) { 920 int customBackgroundColor = getBackgroundColor(mVisibleType); 921 mContainingNotification.resetBackgroundAlpha(); 922 mContainingNotification.setContentBackground(customBackgroundColor, animate, this); 923 } 924 setBackgroundTintColor(int color)925 public void setBackgroundTintColor(int color) { 926 if (mExpandedSmartReplyView != null) { 927 mExpandedSmartReplyView.setBackgroundTintColor(color); 928 } 929 if (mHeadsUpSmartReplyView != null) { 930 mHeadsUpSmartReplyView.setBackgroundTintColor(color); 931 } 932 } 933 getVisibleType()934 public int getVisibleType() { 935 return mVisibleType; 936 } 937 getBackgroundColorForExpansionState()938 public int getBackgroundColorForExpansionState() { 939 // When expanding or user locked we want the new type, when collapsing we want 940 // the original type 941 final int visibleType = (mContainingNotification.isGroupExpanded() 942 || mContainingNotification.isUserLocked()) 943 ? calculateVisibleType() 944 : getVisibleType(); 945 return getBackgroundColor(visibleType); 946 } 947 getBackgroundColor(int visibleType)948 public int getBackgroundColor(int visibleType) { 949 NotificationViewWrapper currentVisibleWrapper = getVisibleWrapper(visibleType); 950 int customBackgroundColor = 0; 951 if (currentVisibleWrapper != null) { 952 customBackgroundColor = currentVisibleWrapper.getCustomBackgroundColor(); 953 } 954 return customBackgroundColor; 955 } 956 updateViewVisibilities(int visibleType)957 private void updateViewVisibilities(int visibleType) { 958 updateViewVisibility(visibleType, VISIBLE_TYPE_CONTRACTED, 959 mContractedChild, mContractedWrapper); 960 updateViewVisibility(visibleType, VISIBLE_TYPE_EXPANDED, 961 mExpandedChild, mExpandedWrapper); 962 updateViewVisibility(visibleType, VISIBLE_TYPE_HEADSUP, 963 mHeadsUpChild, mHeadsUpWrapper); 964 updateViewVisibility(visibleType, VISIBLE_TYPE_SINGLELINE, 965 mSingleLineView, mSingleLineView); 966 updateViewVisibility(visibleType, VISIBLE_TYPE_AMBIENT, 967 mAmbientChild, mAmbientWrapper); 968 fireExpandedVisibleListenerIfVisible(); 969 // updateViewVisibilities cancels outstanding animations without updating the 970 // mAnimationStartVisibleType. Do so here instead. 971 mAnimationStartVisibleType = UNDEFINED; 972 } 973 updateViewVisibility(int visibleType, int type, View view, TransformableView wrapper)974 private void updateViewVisibility(int visibleType, int type, View view, 975 TransformableView wrapper) { 976 if (view != null) { 977 wrapper.setVisible(visibleType == type); 978 } 979 } 980 animateToVisibleType(int visibleType)981 private void animateToVisibleType(int visibleType) { 982 final TransformableView shownView = getTransformableViewForVisibleType(visibleType); 983 final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType); 984 if (shownView == hiddenView || hiddenView == null) { 985 shownView.setVisible(true); 986 return; 987 } 988 mAnimationStartVisibleType = mVisibleType; 989 shownView.transformFrom(hiddenView); 990 getViewForVisibleType(visibleType).setVisibility(View.VISIBLE); 991 hiddenView.transformTo(shownView, new Runnable() { 992 @Override 993 public void run() { 994 if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) { 995 hiddenView.setVisible(false); 996 } 997 mAnimationStartVisibleType = UNDEFINED; 998 } 999 }); 1000 fireExpandedVisibleListenerIfVisible(); 1001 } 1002 transferRemoteInputFocus(int visibleType)1003 private void transferRemoteInputFocus(int visibleType) { 1004 if (visibleType == VISIBLE_TYPE_HEADSUP 1005 && mHeadsUpRemoteInput != null 1006 && (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive())) { 1007 mHeadsUpRemoteInput.stealFocusFrom(mExpandedRemoteInput); 1008 } 1009 if (visibleType == VISIBLE_TYPE_EXPANDED 1010 && mExpandedRemoteInput != null 1011 && (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive())) { 1012 mExpandedRemoteInput.stealFocusFrom(mHeadsUpRemoteInput); 1013 } 1014 } 1015 1016 /** 1017 * @param visibleType one of the static enum types in this view 1018 * @return the corresponding transformable view according to the given visible type 1019 */ getTransformableViewForVisibleType(int visibleType)1020 private TransformableView getTransformableViewForVisibleType(int visibleType) { 1021 switch (visibleType) { 1022 case VISIBLE_TYPE_EXPANDED: 1023 return mExpandedWrapper; 1024 case VISIBLE_TYPE_HEADSUP: 1025 return mHeadsUpWrapper; 1026 case VISIBLE_TYPE_SINGLELINE: 1027 return mSingleLineView; 1028 case VISIBLE_TYPE_AMBIENT: 1029 return mAmbientWrapper; 1030 default: 1031 return mContractedWrapper; 1032 } 1033 } 1034 1035 /** 1036 * @param visibleType one of the static enum types in this view 1037 * @return the corresponding view according to the given visible type 1038 */ getViewForVisibleType(int visibleType)1039 private View getViewForVisibleType(int visibleType) { 1040 switch (visibleType) { 1041 case VISIBLE_TYPE_EXPANDED: 1042 return mExpandedChild; 1043 case VISIBLE_TYPE_HEADSUP: 1044 return mHeadsUpChild; 1045 case VISIBLE_TYPE_SINGLELINE: 1046 return mSingleLineView; 1047 case VISIBLE_TYPE_AMBIENT: 1048 return mAmbientChild; 1049 default: 1050 return mContractedChild; 1051 } 1052 } 1053 getVisibleWrapper(int visibleType)1054 public NotificationViewWrapper getVisibleWrapper(int visibleType) { 1055 switch (visibleType) { 1056 case VISIBLE_TYPE_EXPANDED: 1057 return mExpandedWrapper; 1058 case VISIBLE_TYPE_HEADSUP: 1059 return mHeadsUpWrapper; 1060 case VISIBLE_TYPE_CONTRACTED: 1061 return mContractedWrapper; 1062 case VISIBLE_TYPE_AMBIENT: 1063 return mAmbientWrapper; 1064 default: 1065 return null; 1066 } 1067 } 1068 1069 /** 1070 * @return one of the static enum types in this view, calculated form the current state 1071 */ calculateVisibleType()1072 public int calculateVisibleType() { 1073 if (mUserExpanding) { 1074 int height = !mIsChildInGroup || isGroupExpanded() 1075 || mContainingNotification.isExpanded(true /* allowOnKeyguard */) 1076 ? mContainingNotification.getMaxContentHeight() 1077 : mContainingNotification.getShowingLayout().getMinHeight(); 1078 if (height == 0) { 1079 height = mContentHeight; 1080 } 1081 int expandedVisualType = getVisualTypeForHeight(height); 1082 int collapsedVisualType = mIsChildInGroup && !isGroupExpanded() 1083 ? VISIBLE_TYPE_SINGLELINE 1084 : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight()); 1085 return mTransformationStartVisibleType == collapsedVisualType 1086 ? expandedVisualType 1087 : collapsedVisualType; 1088 } 1089 int intrinsicHeight = mContainingNotification.getIntrinsicHeight(); 1090 int viewHeight = mContentHeight; 1091 if (intrinsicHeight != 0) { 1092 // the intrinsicHeight might be 0 because it was just reset. 1093 viewHeight = Math.min(mContentHeight, intrinsicHeight); 1094 } 1095 return getVisualTypeForHeight(viewHeight); 1096 } 1097 getVisualTypeForHeight(float viewHeight)1098 private int getVisualTypeForHeight(float viewHeight) { 1099 boolean noExpandedChild = mExpandedChild == null; 1100 if (!noExpandedChild && viewHeight == getViewHeight(VISIBLE_TYPE_EXPANDED)) { 1101 return VISIBLE_TYPE_EXPANDED; 1102 } 1103 boolean onAmbient = mContainingNotification.isOnAmbient(); 1104 if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) { 1105 return VISIBLE_TYPE_SINGLELINE; 1106 } 1107 1108 if ((mIsHeadsUp || mHeadsUpAnimatingAway) && mHeadsUpChild != null 1109 && !mContainingNotification.isOnKeyguard()) { 1110 if (viewHeight <= getViewHeight(VISIBLE_TYPE_HEADSUP) || noExpandedChild) { 1111 return VISIBLE_TYPE_HEADSUP; 1112 } else { 1113 return VISIBLE_TYPE_EXPANDED; 1114 } 1115 } else { 1116 int collapsedType = onAmbient && mAmbientChild != null ? VISIBLE_TYPE_AMBIENT : 1117 VISIBLE_TYPE_CONTRACTED; 1118 if (noExpandedChild || (mContractedChild != null 1119 && viewHeight <= getViewHeight(collapsedType) 1120 && (!mIsChildInGroup || isGroupExpanded() 1121 || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) { 1122 return collapsedType; 1123 } else { 1124 return VISIBLE_TYPE_EXPANDED; 1125 } 1126 } 1127 } 1128 isContentExpandable()1129 public boolean isContentExpandable() { 1130 return mIsContentExpandable; 1131 } 1132 setDark(boolean dark, boolean fade, long delay)1133 public void setDark(boolean dark, boolean fade, long delay) { 1134 if (mContractedChild == null) { 1135 return; 1136 } 1137 mDark = dark; 1138 selectLayout(!dark && fade /* animate */, false /* force */); 1139 } 1140 setHeadsUp(boolean headsUp)1141 public void setHeadsUp(boolean headsUp) { 1142 mIsHeadsUp = headsUp; 1143 selectLayout(false /* animate */, true /* force */); 1144 updateExpandButtons(mExpandable); 1145 } 1146 1147 @Override hasOverlappingRendering()1148 public boolean hasOverlappingRendering() { 1149 1150 // This is not really true, but good enough when fading from the contracted to the expanded 1151 // layout, and saves us some layers. 1152 return false; 1153 } 1154 setLegacy(boolean legacy)1155 public void setLegacy(boolean legacy) { 1156 mLegacy = legacy; 1157 updateLegacy(); 1158 } 1159 updateLegacy()1160 private void updateLegacy() { 1161 if (mContractedChild != null) { 1162 mContractedWrapper.setLegacy(mLegacy); 1163 } 1164 if (mExpandedChild != null) { 1165 mExpandedWrapper.setLegacy(mLegacy); 1166 } 1167 if (mHeadsUpChild != null) { 1168 mHeadsUpWrapper.setLegacy(mLegacy); 1169 } 1170 } 1171 setIsChildInGroup(boolean isChildInGroup)1172 public void setIsChildInGroup(boolean isChildInGroup) { 1173 mIsChildInGroup = isChildInGroup; 1174 if (mContractedChild != null) { 1175 mContractedWrapper.setIsChildInGroup(mIsChildInGroup); 1176 } 1177 if (mExpandedChild != null) { 1178 mExpandedWrapper.setIsChildInGroup(mIsChildInGroup); 1179 } 1180 if (mHeadsUpChild != null) { 1181 mHeadsUpWrapper.setIsChildInGroup(mIsChildInGroup); 1182 } 1183 if (mAmbientChild != null) { 1184 mAmbientWrapper.setIsChildInGroup(mIsChildInGroup); 1185 } 1186 updateAllSingleLineViews(); 1187 } 1188 onNotificationUpdated(NotificationEntry entry)1189 public void onNotificationUpdated(NotificationEntry entry) { 1190 mStatusBarNotification = entry.notification; 1191 mOnContentViewInactiveListeners.clear(); 1192 mBeforeN = entry.targetSdk < Build.VERSION_CODES.N; 1193 updateAllSingleLineViews(); 1194 ExpandableNotificationRow row = entry.getRow(); 1195 if (mContractedChild != null) { 1196 mContractedWrapper.onContentUpdated(row); 1197 } 1198 if (mExpandedChild != null) { 1199 mExpandedWrapper.onContentUpdated(row); 1200 } 1201 if (mHeadsUpChild != null) { 1202 mHeadsUpWrapper.onContentUpdated(row); 1203 } 1204 if (mAmbientChild != null) { 1205 mAmbientWrapper.onContentUpdated(row); 1206 } 1207 applyRemoteInputAndSmartReply(entry); 1208 applyMediaTransfer(entry); 1209 updateLegacy(); 1210 mForceSelectNextLayout = true; 1211 setDark(mDark, false /* animate */, 0 /* delay */); 1212 mPreviousExpandedRemoteInputIntent = null; 1213 mPreviousHeadsUpRemoteInputIntent = null; 1214 } 1215 1216 private void updateAllSingleLineViews() { 1217 updateSingleLineView(); 1218 } 1219 1220 private void updateSingleLineView() { 1221 if (mIsChildInGroup) { 1222 boolean isNewView = mSingleLineView == null; 1223 mSingleLineView = mHybridGroupManager.bindFromNotification( 1224 mSingleLineView, mStatusBarNotification.getNotification()); 1225 if (isNewView) { 1226 updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE, 1227 mSingleLineView, mSingleLineView); 1228 } 1229 } else if (mSingleLineView != null) { 1230 removeView(mSingleLineView); 1231 mSingleLineView = null; 1232 } 1233 } 1234 1235 private void applyMediaTransfer(final NotificationEntry entry) { 1236 View bigContentView = mExpandedChild; 1237 if (bigContentView == null || !entry.isMediaNotification()) { 1238 return; 1239 } 1240 1241 View mediaActionContainer = bigContentView.findViewById( 1242 com.android.internal.R.id.media_actions); 1243 if (!(mediaActionContainer instanceof LinearLayout)) { 1244 return; 1245 } 1246 1247 mMediaTransferManager.applyMediaTransferView((ViewGroup) mediaActionContainer, 1248 entry); 1249 } 1250 1251 private void applyRemoteInputAndSmartReply(final NotificationEntry entry) { 1252 if (mRemoteInputController == null) { 1253 return; 1254 } 1255 1256 applyRemoteInput(entry, InflatedSmartReplies.hasFreeformRemoteInput(entry)); 1257 1258 if (mExpandedInflatedSmartReplies == null && mHeadsUpInflatedSmartReplies == null) { 1259 if (DEBUG) { 1260 Log.d(TAG, "Both expanded, and heads-up InflatedSmartReplies are null, " 1261 + "don't add smart replies."); 1262 } 1263 return; 1264 } 1265 // The inflated smart-reply objects for the expanded view and the heads-up view both contain 1266 // the same SmartRepliesAndActions to avoid discrepancies between the two views. We here 1267 // reuse that object for our local SmartRepliesAndActions to avoid discrepancies between 1268 // this class and the InflatedSmartReplies classes. 1269 mCurrentSmartRepliesAndActions = mExpandedInflatedSmartReplies != null 1270 ? mExpandedInflatedSmartReplies.getSmartRepliesAndActions() 1271 : mHeadsUpInflatedSmartReplies.getSmartRepliesAndActions(); 1272 if (DEBUG) { 1273 Log.d(TAG, String.format("Adding suggestions for %s, %d actions, and %d replies.", 1274 entry.notification.getKey(), 1275 mCurrentSmartRepliesAndActions.smartActions == null ? 0 : 1276 mCurrentSmartRepliesAndActions.smartActions.actions.size(), 1277 mCurrentSmartRepliesAndActions.smartReplies == null ? 0 : 1278 mCurrentSmartRepliesAndActions.smartReplies.choices.length)); 1279 } 1280 applySmartReplyView(mCurrentSmartRepliesAndActions, entry); 1281 } 1282 1283 private void applyRemoteInput(NotificationEntry entry, boolean hasFreeformRemoteInput) { 1284 View bigContentView = mExpandedChild; 1285 if (bigContentView != null) { 1286 mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasFreeformRemoteInput, 1287 mPreviousExpandedRemoteInputIntent, mCachedExpandedRemoteInput, 1288 mExpandedWrapper); 1289 } else { 1290 mExpandedRemoteInput = null; 1291 } 1292 if (mCachedExpandedRemoteInput != null 1293 && mCachedExpandedRemoteInput != mExpandedRemoteInput) { 1294 // We had a cached remote input but didn't reuse it. Clean up required. 1295 mCachedExpandedRemoteInput.dispatchFinishTemporaryDetach(); 1296 } 1297 mCachedExpandedRemoteInput = null; 1298 1299 View headsUpContentView = mHeadsUpChild; 1300 if (headsUpContentView != null) { 1301 mHeadsUpRemoteInput = applyRemoteInput( 1302 headsUpContentView, entry, hasFreeformRemoteInput, 1303 mPreviousHeadsUpRemoteInputIntent, mCachedHeadsUpRemoteInput, mHeadsUpWrapper); 1304 } else { 1305 mHeadsUpRemoteInput = null; 1306 } 1307 if (mCachedHeadsUpRemoteInput != null 1308 && mCachedHeadsUpRemoteInput != mHeadsUpRemoteInput) { 1309 // We had a cached remote input but didn't reuse it. Clean up required. 1310 mCachedHeadsUpRemoteInput.dispatchFinishTemporaryDetach(); 1311 } 1312 mCachedHeadsUpRemoteInput = null; 1313 } 1314 1315 private RemoteInputView applyRemoteInput(View view, NotificationEntry entry, 1316 boolean hasRemoteInput, PendingIntent existingPendingIntent, 1317 RemoteInputView cachedView, NotificationViewWrapper wrapper) { 1318 View actionContainerCandidate = view.findViewById( 1319 com.android.internal.R.id.actions_container); 1320 if (actionContainerCandidate instanceof FrameLayout) { 1321 RemoteInputView existing = (RemoteInputView) 1322 view.findViewWithTag(RemoteInputView.VIEW_TAG); 1323 1324 if (existing != null) { 1325 existing.onNotificationUpdateOrReset(); 1326 } 1327 1328 if (existing == null && hasRemoteInput) { 1329 ViewGroup actionContainer = (FrameLayout) actionContainerCandidate; 1330 if (cachedView == null) { 1331 RemoteInputView riv = RemoteInputView.inflate( 1332 mContext, actionContainer, entry, mRemoteInputController); 1333 1334 riv.setVisibility(View.INVISIBLE); 1335 actionContainer.addView(riv, new LayoutParams( 1336 ViewGroup.LayoutParams.MATCH_PARENT, 1337 ViewGroup.LayoutParams.MATCH_PARENT) 1338 ); 1339 existing = riv; 1340 } else { 1341 actionContainer.addView(cachedView); 1342 cachedView.dispatchFinishTemporaryDetach(); 1343 cachedView.requestFocus(); 1344 existing = cachedView; 1345 } 1346 } 1347 if (hasRemoteInput) { 1348 int color = entry.notification.getNotification().color; 1349 if (color == Notification.COLOR_DEFAULT) { 1350 color = mContext.getColor(R.color.default_remote_input_background); 1351 } 1352 existing.setBackgroundColor(ContrastColorUtil.ensureTextBackgroundColor(color, 1353 mContext.getColor(R.color.remote_input_text_enabled), 1354 mContext.getColor(R.color.remote_input_hint))); 1355 1356 existing.setWrapper(wrapper); 1357 existing.setOnVisibilityChangedListener(this::setRemoteInputVisible); 1358 1359 if (existingPendingIntent != null || existing.isActive()) { 1360 // The current action could be gone, or the pending intent no longer valid. 1361 // If we find a matching action in the new notification, focus, otherwise close. 1362 Notification.Action[] actions = entry.notification.getNotification().actions; 1363 if (existingPendingIntent != null) { 1364 existing.setPendingIntent(existingPendingIntent); 1365 } 1366 if (existing.updatePendingIntentFromActions(actions)) { 1367 if (!existing.isActive()) { 1368 existing.focus(); 1369 } 1370 } else { 1371 if (existing.isActive()) { 1372 existing.close(); 1373 } 1374 } 1375 } 1376 } 1377 return existing; 1378 } 1379 return null; 1380 } 1381 1382 private void applySmartReplyView( 1383 SmartRepliesAndActions smartRepliesAndActions, 1384 NotificationEntry entry) { 1385 if (mExpandedChild != null) { 1386 mExpandedSmartReplyView = applySmartReplyView(mExpandedChild, smartRepliesAndActions, 1387 entry, mExpandedInflatedSmartReplies); 1388 if (mExpandedSmartReplyView != null) { 1389 if (smartRepliesAndActions.smartReplies != null 1390 || smartRepliesAndActions.smartActions != null) { 1391 int numSmartReplies = smartRepliesAndActions.smartReplies == null 1392 ? 0 : smartRepliesAndActions.smartReplies.choices.length; 1393 int numSmartActions = smartRepliesAndActions.smartActions == null 1394 ? 0 : smartRepliesAndActions.smartActions.actions.size(); 1395 boolean fromAssistant = smartRepliesAndActions.smartReplies == null 1396 ? smartRepliesAndActions.smartActions.fromAssistant 1397 : smartRepliesAndActions.smartReplies.fromAssistant; 1398 boolean editBeforeSending = smartRepliesAndActions.smartReplies != null 1399 && mSmartReplyConstants.getEffectiveEditChoicesBeforeSending( 1400 smartRepliesAndActions.smartReplies.remoteInput 1401 .getEditChoicesBeforeSending()); 1402 1403 mSmartReplyController.smartSuggestionsAdded(entry, numSmartReplies, 1404 numSmartActions, fromAssistant, editBeforeSending); 1405 } 1406 } 1407 } 1408 if (mHeadsUpChild != null && mSmartReplyConstants.getShowInHeadsUp()) { 1409 mHeadsUpSmartReplyView = applySmartReplyView(mHeadsUpChild, smartRepliesAndActions, 1410 entry, mHeadsUpInflatedSmartReplies); 1411 } 1412 } 1413 1414 @Nullable 1415 private SmartReplyView applySmartReplyView(View view, 1416 SmartRepliesAndActions smartRepliesAndActions, 1417 NotificationEntry entry, InflatedSmartReplies inflatedSmartReplyView) { 1418 View smartReplyContainerCandidate = view.findViewById( 1419 com.android.internal.R.id.smart_reply_container); 1420 if (!(smartReplyContainerCandidate instanceof LinearLayout)) { 1421 return null; 1422 } 1423 1424 LinearLayout smartReplyContainer = (LinearLayout) smartReplyContainerCandidate; 1425 if (!InflatedSmartReplies.shouldShowSmartReplyView(entry, smartRepliesAndActions)) { 1426 smartReplyContainer.setVisibility(View.GONE); 1427 return null; 1428 } 1429 1430 SmartReplyView smartReplyView = null; 1431 if (smartReplyContainer.getChildCount() == 1 1432 && smartReplyContainer.getChildAt(0) instanceof SmartReplyView) { 1433 // If we already have a SmartReplyView - replace it with the newly inflated one. The 1434 // newly inflated one is connected to the new inflated smart reply/action buttons. 1435 smartReplyContainer.removeAllViews(); 1436 } 1437 if (smartReplyContainer.getChildCount() == 0 1438 && inflatedSmartReplyView != null 1439 && inflatedSmartReplyView.getSmartReplyView() != null) { 1440 smartReplyView = inflatedSmartReplyView.getSmartReplyView(); 1441 smartReplyContainer.addView(smartReplyView); 1442 } 1443 if (smartReplyView != null) { 1444 smartReplyView.resetSmartSuggestions(smartReplyContainer); 1445 smartReplyView.addPreInflatedButtons( 1446 inflatedSmartReplyView.getSmartSuggestionButtons()); 1447 // Ensure the colors of the smart suggestion buttons are up-to-date. 1448 smartReplyView.setBackgroundTintColor(entry.getRow().getCurrentBackgroundTint()); 1449 smartReplyContainer.setVisibility(View.VISIBLE); 1450 } 1451 return smartReplyView; 1452 } 1453 1454 /** 1455 * Set pre-inflated views necessary to display smart replies and actions in the expanded 1456 * notification state. 1457 * 1458 * @param inflatedSmartReplies the pre-inflated state to add to this view. If null the existing 1459 * {@link SmartReplyView} related to the expanded notification state is cleared. 1460 */ 1461 public void setExpandedInflatedSmartReplies( 1462 @Nullable InflatedSmartReplies inflatedSmartReplies) { 1463 mExpandedInflatedSmartReplies = inflatedSmartReplies; 1464 if (inflatedSmartReplies == null) { 1465 mExpandedSmartReplyView = null; 1466 } 1467 } 1468 1469 /** 1470 * Set pre-inflated views necessary to display smart replies and actions in the heads-up 1471 * notification state. 1472 * 1473 * @param inflatedSmartReplies the pre-inflated state to add to this view. If null the existing 1474 * {@link SmartReplyView} related to the heads-up notification state is cleared. 1475 */ 1476 public void setHeadsUpInflatedSmartReplies( 1477 @Nullable InflatedSmartReplies inflatedSmartReplies) { 1478 mHeadsUpInflatedSmartReplies = inflatedSmartReplies; 1479 if (inflatedSmartReplies == null) { 1480 mHeadsUpSmartReplyView = null; 1481 } 1482 } 1483 1484 /** 1485 * Returns the smart replies and actions currently shown in the notification. 1486 */ 1487 @Nullable public SmartRepliesAndActions getCurrentSmartRepliesAndActions() { 1488 return mCurrentSmartRepliesAndActions; 1489 } 1490 1491 public void closeRemoteInput() { 1492 if (mHeadsUpRemoteInput != null) { 1493 mHeadsUpRemoteInput.close(); 1494 } 1495 if (mExpandedRemoteInput != null) { 1496 mExpandedRemoteInput.close(); 1497 } 1498 } 1499 1500 public void setGroupManager(NotificationGroupManager groupManager) { 1501 mGroupManager = groupManager; 1502 } 1503 1504 public void setRemoteInputController(RemoteInputController r) { 1505 mRemoteInputController = r; 1506 } 1507 1508 public void setExpandClickListener(OnClickListener expandClickListener) { 1509 mExpandClickListener = expandClickListener; 1510 } 1511 1512 public void updateExpandButtons(boolean expandable) { 1513 mExpandable = expandable; 1514 // if the expanded child has the same height as the collapsed one we hide it. 1515 if (mExpandedChild != null && mExpandedChild.getHeight() != 0) { 1516 if ((!mIsHeadsUp && !mHeadsUpAnimatingAway) 1517 || mHeadsUpChild == null || mContainingNotification.isOnKeyguard()) { 1518 if (mExpandedChild.getHeight() <= mContractedChild.getHeight()) { 1519 expandable = false; 1520 } 1521 } else if (mExpandedChild.getHeight() <= mHeadsUpChild.getHeight()) { 1522 expandable = false; 1523 } 1524 } 1525 if (mExpandedChild != null) { 1526 mExpandedWrapper.updateExpandability(expandable, mExpandClickListener); 1527 } 1528 if (mContractedChild != null) { 1529 mContractedWrapper.updateExpandability(expandable, mExpandClickListener); 1530 } 1531 if (mHeadsUpChild != null) { 1532 mHeadsUpWrapper.updateExpandability(expandable, mExpandClickListener); 1533 } 1534 mIsContentExpandable = expandable; 1535 } 1536 1537 public NotificationHeaderView getNotificationHeader() { 1538 NotificationHeaderView header = null; 1539 if (mContractedChild != null) { 1540 header = mContractedWrapper.getNotificationHeader(); 1541 } 1542 if (header == null && mExpandedChild != null) { 1543 header = mExpandedWrapper.getNotificationHeader(); 1544 } 1545 if (header == null && mHeadsUpChild != null) { 1546 header = mHeadsUpWrapper.getNotificationHeader(); 1547 } 1548 if (header == null && mAmbientChild != null) { 1549 header = mAmbientWrapper.getNotificationHeader(); 1550 } 1551 return header; 1552 } 1553 1554 public void showAppOpsIcons(ArraySet<Integer> activeOps) { 1555 if (mContractedChild != null && mContractedWrapper.getNotificationHeader() != null) { 1556 mContractedWrapper.getNotificationHeader().showAppOpsIcons(activeOps); 1557 } 1558 if (mExpandedChild != null && mExpandedWrapper.getNotificationHeader() != null) { 1559 mExpandedWrapper.getNotificationHeader().showAppOpsIcons(activeOps); 1560 } 1561 if (mHeadsUpChild != null && mHeadsUpWrapper.getNotificationHeader() != null) { 1562 mHeadsUpWrapper.getNotificationHeader().showAppOpsIcons(activeOps); 1563 } 1564 } 1565 1566 /** Sets whether the notification being displayed audibly alerted the user. */ 1567 public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) { 1568 if (mContractedChild != null && mContractedWrapper.getNotificationHeader() != null) { 1569 mContractedWrapper.getNotificationHeader().setRecentlyAudiblyAlerted(audiblyAlerted); 1570 } 1571 if (mExpandedChild != null && mExpandedWrapper.getNotificationHeader() != null) { 1572 mExpandedWrapper.getNotificationHeader().setRecentlyAudiblyAlerted(audiblyAlerted); 1573 } 1574 if (mHeadsUpChild != null && mHeadsUpWrapper.getNotificationHeader() != null) { 1575 mHeadsUpWrapper.getNotificationHeader().setRecentlyAudiblyAlerted(audiblyAlerted); 1576 } 1577 } 1578 1579 public NotificationHeaderView getContractedNotificationHeader() { 1580 if (mContractedChild != null) { 1581 return mContractedWrapper.getNotificationHeader(); 1582 } 1583 return null; 1584 } 1585 1586 public NotificationHeaderView getVisibleNotificationHeader() { 1587 NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); 1588 return wrapper == null ? null : wrapper.getNotificationHeader(); 1589 } 1590 1591 public void setContainingNotification(ExpandableNotificationRow containingNotification) { 1592 mContainingNotification = containingNotification; 1593 } 1594 1595 public void requestSelectLayout(boolean needsAnimation) { 1596 selectLayout(needsAnimation, false); 1597 } 1598 1599 public void reInflateViews() { 1600 if (mIsChildInGroup && mSingleLineView != null) { 1601 removeView(mSingleLineView); 1602 mSingleLineView = null; 1603 updateAllSingleLineViews(); 1604 } 1605 } 1606 1607 public void setUserExpanding(boolean userExpanding) { 1608 mUserExpanding = userExpanding; 1609 if (userExpanding) { 1610 mTransformationStartVisibleType = mVisibleType; 1611 } else { 1612 mTransformationStartVisibleType = UNDEFINED; 1613 mVisibleType = calculateVisibleType(); 1614 updateViewVisibilities(mVisibleType); 1615 updateBackgroundColor(false); 1616 } 1617 } 1618 1619 /** 1620 * Set by how much the single line view should be indented. Used when a overflow indicator is 1621 * present and only during measuring 1622 */ 1623 public void setSingleLineWidthIndention(int singleLineWidthIndention) { 1624 if (singleLineWidthIndention != mSingleLineWidthIndention) { 1625 mSingleLineWidthIndention = singleLineWidthIndention; 1626 mContainingNotification.forceLayout(); 1627 forceLayout(); 1628 } 1629 } 1630 1631 public HybridNotificationView getSingleLineView() { 1632 return mSingleLineView; 1633 } 1634 1635 public void setRemoved() { 1636 if (mExpandedRemoteInput != null) { 1637 mExpandedRemoteInput.setRemoved(); 1638 } 1639 if (mHeadsUpRemoteInput != null) { 1640 mHeadsUpRemoteInput.setRemoved(); 1641 } 1642 } 1643 1644 public void setContentHeightAnimating(boolean animating) { 1645 if (!animating) { 1646 mContentHeightAtAnimationStart = UNDEFINED; 1647 } 1648 } 1649 1650 @VisibleForTesting 1651 boolean isAnimatingVisibleType() { 1652 return mAnimationStartVisibleType != UNDEFINED; 1653 } 1654 1655 public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { 1656 mHeadsUpAnimatingAway = headsUpAnimatingAway; 1657 selectLayout(false /* animate */, true /* force */); 1658 } 1659 1660 public void setFocusOnVisibilityChange() { 1661 mFocusOnVisibilityChange = true; 1662 } 1663 1664 public void setIconsVisible(boolean iconsVisible) { 1665 mIconsVisible = iconsVisible; 1666 updateIconVisibilities(); 1667 } 1668 1669 private void updateIconVisibilities() { 1670 if (mContractedWrapper != null) { 1671 NotificationHeaderView header = mContractedWrapper.getNotificationHeader(); 1672 if (header != null) { 1673 header.getIcon().setForceHidden(!mIconsVisible); 1674 } 1675 } 1676 if (mHeadsUpWrapper != null) { 1677 NotificationHeaderView header = mHeadsUpWrapper.getNotificationHeader(); 1678 if (header != null) { 1679 header.getIcon().setForceHidden(!mIconsVisible); 1680 } 1681 } 1682 if (mExpandedWrapper != null) { 1683 NotificationHeaderView header = mExpandedWrapper.getNotificationHeader(); 1684 if (header != null) { 1685 header.getIcon().setForceHidden(!mIconsVisible); 1686 } 1687 } 1688 } 1689 1690 @Override 1691 public void onVisibilityAggregated(boolean isVisible) { 1692 super.onVisibilityAggregated(isVisible); 1693 if (isVisible) { 1694 fireExpandedVisibleListenerIfVisible(); 1695 } 1696 } 1697 1698 /** 1699 * Sets a one-shot listener for when the expanded view becomes visible. 1700 * 1701 * This will fire the listener immediately if the expanded view is already visible. 1702 */ 1703 public void setOnExpandedVisibleListener(Runnable r) { 1704 mExpandedVisibleListener = r; 1705 fireExpandedVisibleListenerIfVisible(); 1706 } 1707 1708 /** 1709 * Set a one-shot listener to run when a given content view becomes inactive. 1710 * 1711 * @param visibleType visible type corresponding to the content view to listen 1712 * @param listener runnable to run once when the content view becomes inactive 1713 */ 1714 public void performWhenContentInactive(int visibleType, Runnable listener) { 1715 View view = getViewForVisibleType(visibleType); 1716 // View is already inactive 1717 if (view == null || isContentViewInactive(visibleType)) { 1718 listener.run(); 1719 return; 1720 } 1721 mOnContentViewInactiveListeners.put(view, listener); 1722 } 1723 1724 /** 1725 * Whether or not the content view is inactive. This means it should not be visible 1726 * or the showing content as removing it would cause visual jank. 1727 * 1728 * @param visibleType visible type corresponding to the content view to be removed 1729 * @return true if the content view is inactive, false otherwise 1730 */ 1731 public boolean isContentViewInactive(int visibleType) { 1732 View view = getViewForVisibleType(visibleType); 1733 return isContentViewInactive(view); 1734 } 1735 1736 /** 1737 * Whether or not the content view is inactive. 1738 * 1739 * @param view view to see if its inactive 1740 * @return true if the view is inactive, false o/w 1741 */ 1742 private boolean isContentViewInactive(View view) { 1743 if (view == null) { 1744 return true; 1745 } 1746 return !isShown() 1747 || (view.getVisibility() != VISIBLE && getViewForVisibleType(mVisibleType) != view); 1748 } 1749 1750 @Override 1751 protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) { 1752 super.onChildVisibilityChanged(child, oldVisibility, newVisibility); 1753 if (isContentViewInactive(child)) { 1754 Runnable listener = mOnContentViewInactiveListeners.remove(child); 1755 if (listener != null) { 1756 listener.run(); 1757 } 1758 } 1759 } 1760 1761 public void setIsLowPriority(boolean isLowPriority) { 1762 mIsLowPriority = isLowPriority; 1763 } 1764 1765 public boolean isDimmable() { 1766 return mContractedWrapper != null && mContractedWrapper.isDimmable(); 1767 } 1768 1769 /** 1770 * Should a single click be disallowed on this view when on the keyguard? 1771 */ 1772 public boolean disallowSingleClick(float x, float y) { 1773 NotificationViewWrapper visibleWrapper = getVisibleWrapper(getVisibleType()); 1774 if (visibleWrapper != null) { 1775 return visibleWrapper.disallowSingleClick(x, y); 1776 } 1777 return false; 1778 } 1779 1780 public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) { 1781 boolean needsPaddings = shouldClipToRounding(getVisibleType(), topRounded, bottomRounded); 1782 if (mUserExpanding) { 1783 needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded, 1784 bottomRounded); 1785 } 1786 return needsPaddings; 1787 } 1788 1789 private boolean shouldClipToRounding(int visibleType, boolean topRounded, 1790 boolean bottomRounded) { 1791 NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType); 1792 if (visibleWrapper == null) { 1793 return false; 1794 } 1795 return visibleWrapper.shouldClipToRounding(topRounded, bottomRounded); 1796 } 1797 1798 public CharSequence getActiveRemoteInputText() { 1799 if (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive()) { 1800 return mExpandedRemoteInput.getText(); 1801 } 1802 if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive()) { 1803 return mHeadsUpRemoteInput.getText(); 1804 } 1805 return null; 1806 } 1807 1808 @Override 1809 public boolean dispatchTouchEvent(MotionEvent ev) { 1810 float y = ev.getY(); 1811 // We still want to distribute touch events to the remote input even if it's outside the 1812 // view boundary. We're therefore manually dispatching these events to the remote view 1813 RemoteInputView riv = getRemoteInputForView(getViewForVisibleType(mVisibleType)); 1814 if (riv != null && riv.getVisibility() == VISIBLE) { 1815 int inputStart = mUnrestrictedContentHeight - riv.getHeight(); 1816 if (y <= mUnrestrictedContentHeight && y >= inputStart) { 1817 ev.offsetLocation(0, -inputStart); 1818 return riv.dispatchTouchEvent(ev); 1819 } 1820 } 1821 return super.dispatchTouchEvent(ev); 1822 } 1823 1824 /** 1825 * Overridden to make sure touches to the reply action bar actually go through to this view 1826 */ 1827 @Override 1828 public boolean pointInView(float localX, float localY, float slop) { 1829 float top = mClipTopAmount; 1830 float bottom = mUnrestrictedContentHeight; 1831 return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) && 1832 localY < (bottom + slop); 1833 } 1834 1835 private RemoteInputView getRemoteInputForView(View child) { 1836 if (child == mExpandedChild) { 1837 return mExpandedRemoteInput; 1838 } else if (child == mHeadsUpChild) { 1839 return mHeadsUpRemoteInput; 1840 } 1841 return null; 1842 } 1843 1844 public int getExpandHeight() { 1845 int viewType = VISIBLE_TYPE_EXPANDED; 1846 if (mExpandedChild == null) { 1847 viewType = VISIBLE_TYPE_CONTRACTED; 1848 } 1849 return getViewHeight(viewType) + getExtraRemoteInputHeight(mExpandedRemoteInput); 1850 } 1851 1852 public int getHeadsUpHeight() { 1853 int viewType = VISIBLE_TYPE_HEADSUP; 1854 if (mHeadsUpChild == null) { 1855 viewType = VISIBLE_TYPE_CONTRACTED; 1856 } 1857 // The headsUp remote input quickly switches to the expanded one, so lets also include that 1858 // one 1859 return getViewHeight(viewType) + getExtraRemoteInputHeight(mHeadsUpRemoteInput) 1860 + getExtraRemoteInputHeight(mExpandedRemoteInput); 1861 } 1862 1863 public void setRemoteInputVisible(boolean remoteInputVisible) { 1864 mRemoteInputVisible = remoteInputVisible; 1865 setClipChildren(!remoteInputVisible); 1866 } 1867 1868 @Override 1869 public void setClipChildren(boolean clipChildren) { 1870 clipChildren = clipChildren && !mRemoteInputVisible; 1871 super.setClipChildren(clipChildren); 1872 } 1873 1874 public void setHeaderVisibleAmount(float headerVisibleAmount) { 1875 if (mContractedWrapper != null) { 1876 mContractedWrapper.setHeaderVisibleAmount(headerVisibleAmount); 1877 } 1878 if (mHeadsUpWrapper != null) { 1879 mHeadsUpWrapper.setHeaderVisibleAmount(headerVisibleAmount); 1880 } 1881 if (mExpandedWrapper != null) { 1882 mExpandedWrapper.setHeaderVisibleAmount(headerVisibleAmount); 1883 } 1884 } 1885 1886 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1887 pw.print(" "); 1888 pw.print("contentView visibility: " + getVisibility()); 1889 pw.print(", alpha: " + getAlpha()); 1890 pw.print(", clipBounds: " + getClipBounds()); 1891 pw.print(", contentHeight: " + mContentHeight); 1892 pw.print(", visibleType: " + mVisibleType); 1893 View view = getViewForVisibleType(mVisibleType); 1894 pw.print(", visibleView "); 1895 if (view != null) { 1896 pw.print(" visibility: " + view.getVisibility()); 1897 pw.print(", alpha: " + view.getAlpha()); 1898 pw.print(", clipBounds: " + view.getClipBounds()); 1899 } else { 1900 pw.print("null"); 1901 } 1902 pw.println(); 1903 } 1904 1905 public RemoteInputView getExpandedRemoteInput() { 1906 return mExpandedRemoteInput; 1907 } 1908 } 1909