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