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