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