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