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