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