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