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