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; 18 19 import android.app.Notification; 20 import android.app.PendingIntent; 21 import android.app.RemoteInput; 22 import android.content.Context; 23 import android.graphics.Rect; 24 import android.os.Build; 25 import android.service.notification.StatusBarNotification; 26 import android.util.AttributeSet; 27 import android.view.NotificationHeaderView; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.ViewTreeObserver; 31 import android.widget.FrameLayout; 32 import android.widget.ImageView; 33 34 import com.android.internal.util.NotificationColorUtil; 35 import com.android.systemui.R; 36 import com.android.systemui.statusbar.notification.HybridNotificationView; 37 import com.android.systemui.statusbar.notification.HybridGroupManager; 38 import com.android.systemui.statusbar.notification.NotificationCustomViewWrapper; 39 import com.android.systemui.statusbar.notification.NotificationUtils; 40 import com.android.systemui.statusbar.notification.NotificationViewWrapper; 41 import com.android.systemui.statusbar.phone.NotificationGroupManager; 42 import com.android.systemui.statusbar.policy.RemoteInputView; 43 44 /** 45 * A frame layout containing the actual payload of the notification, including the contracted, 46 * expanded and heads up layout. This class is responsible for clipping the content and and 47 * switching between the expanded, contracted and the heads up view depending on its clipped size. 48 */ 49 public class NotificationContentView extends FrameLayout { 50 51 private static final int VISIBLE_TYPE_CONTRACTED = 0; 52 private static final int VISIBLE_TYPE_EXPANDED = 1; 53 private static final int VISIBLE_TYPE_HEADSUP = 2; 54 private static final int VISIBLE_TYPE_SINGLELINE = 3; 55 public static final int UNDEFINED = -1; 56 57 private final Rect mClipBounds = new Rect(); 58 private final int mMinContractedHeight; 59 private final int mNotificationContentMarginEnd; 60 61 private View mContractedChild; 62 private View mExpandedChild; 63 private View mHeadsUpChild; 64 private HybridNotificationView mSingleLineView; 65 66 private RemoteInputView mExpandedRemoteInput; 67 private RemoteInputView mHeadsUpRemoteInput; 68 69 private NotificationViewWrapper mContractedWrapper; 70 private NotificationViewWrapper mExpandedWrapper; 71 private NotificationViewWrapper mHeadsUpWrapper; 72 private HybridGroupManager mHybridGroupManager; 73 private int mClipTopAmount; 74 private int mContentHeight; 75 private int mVisibleType = VISIBLE_TYPE_CONTRACTED; 76 private boolean mDark; 77 private boolean mAnimate; 78 private boolean mIsHeadsUp; 79 private boolean mShowingLegacyBackground; 80 private boolean mIsChildInGroup; 81 private int mSmallHeight; 82 private int mHeadsUpHeight; 83 private int mNotificationMaxHeight; 84 private StatusBarNotification mStatusBarNotification; 85 private NotificationGroupManager mGroupManager; 86 private RemoteInputController mRemoteInputController; 87 88 private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener 89 = new ViewTreeObserver.OnPreDrawListener() { 90 @Override 91 public boolean onPreDraw() { 92 // We need to post since we don't want the notification to animate on the very first 93 // frame 94 post(new Runnable() { 95 @Override 96 public void run() { 97 mAnimate = true; 98 } 99 }); 100 getViewTreeObserver().removeOnPreDrawListener(this); 101 return true; 102 } 103 }; 104 105 private OnClickListener mExpandClickListener; 106 private boolean mBeforeN; 107 private boolean mExpandable; 108 private boolean mClipToActualHeight = true; 109 private ExpandableNotificationRow mContainingNotification; 110 /** The visible type at the start of a touch driven transformation */ 111 private int mTransformationStartVisibleType; 112 /** The visible type at the start of an animation driven transformation */ 113 private int mAnimationStartVisibleType = UNDEFINED; 114 private boolean mUserExpanding; 115 private int mSingleLineWidthIndention; 116 private boolean mForceSelectNextLayout = true; 117 private PendingIntent mPreviousExpandedRemoteInputIntent; 118 private PendingIntent mPreviousHeadsUpRemoteInputIntent; 119 120 private int mContentHeightAtAnimationStart = UNDEFINED; 121 private boolean mFocusOnVisibilityChange; 122 private boolean mHeadsupDisappearRunning; 123 124 125 public NotificationContentView(Context context, AttributeSet attrs) { 126 super(context, attrs); 127 mHybridGroupManager = new HybridGroupManager(getContext(), this); 128 mMinContractedHeight = getResources().getDimensionPixelSize( 129 R.dimen.min_notification_layout_height); 130 mNotificationContentMarginEnd = getResources().getDimensionPixelSize( 131 com.android.internal.R.dimen.notification_content_margin_end); 132 reset(); 133 } 134 135 public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight) { 136 mSmallHeight = smallHeight; 137 mHeadsUpHeight = headsUpMaxHeight; 138 mNotificationMaxHeight = maxHeight; 139 } 140 141 @Override 142 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 143 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 144 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; 145 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; 146 int maxSize = Integer.MAX_VALUE; 147 int width = MeasureSpec.getSize(widthMeasureSpec); 148 if (hasFixedHeight || isHeightLimited) { 149 maxSize = MeasureSpec.getSize(heightMeasureSpec); 150 } 151 int maxChildHeight = 0; 152 if (mExpandedChild != null) { 153 int size = Math.min(maxSize, mNotificationMaxHeight); 154 ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams(); 155 if (layoutParams.height >= 0) { 156 // An actual height is set 157 size = Math.min(maxSize, layoutParams.height); 158 } 159 int spec = size == Integer.MAX_VALUE 160 ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) 161 : MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); 162 mExpandedChild.measure(widthMeasureSpec, spec); 163 maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight()); 164 } 165 if (mContractedChild != null) { 166 int heightSpec; 167 int size = Math.min(maxSize, mSmallHeight); 168 if (shouldContractedBeFixedSize()) { 169 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 170 } else { 171 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST); 172 } 173 mContractedChild.measure(widthMeasureSpec, heightSpec); 174 int measuredHeight = mContractedChild.getMeasuredHeight(); 175 if (measuredHeight < mMinContractedHeight) { 176 heightSpec = MeasureSpec.makeMeasureSpec(mMinContractedHeight, MeasureSpec.EXACTLY); 177 mContractedChild.measure(widthMeasureSpec, heightSpec); 178 } 179 maxChildHeight = Math.max(maxChildHeight, measuredHeight); 180 if (updateContractedHeaderWidth()) { 181 mContractedChild.measure(widthMeasureSpec, heightSpec); 182 } 183 if (mExpandedChild != null 184 && mContractedChild.getMeasuredHeight() > mExpandedChild.getMeasuredHeight()) { 185 // the Expanded child is smaller then the collapsed. Let's remeasure it. 186 heightSpec = MeasureSpec.makeMeasureSpec(mContractedChild.getMeasuredHeight(), 187 MeasureSpec.EXACTLY); 188 mExpandedChild.measure(widthMeasureSpec, heightSpec); 189 } 190 } 191 if (mHeadsUpChild != null) { 192 int size = Math.min(maxSize, mHeadsUpHeight); 193 ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams(); 194 if (layoutParams.height >= 0) { 195 // An actual height is set 196 size = Math.min(size, layoutParams.height); 197 } 198 mHeadsUpChild.measure(widthMeasureSpec, 199 MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST)); 200 maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight()); 201 } 202 if (mSingleLineView != null) { 203 int singleLineWidthSpec = widthMeasureSpec; 204 if (mSingleLineWidthIndention != 0 205 && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { 206 singleLineWidthSpec = MeasureSpec.makeMeasureSpec( 207 width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(), 208 MeasureSpec.AT_MOST); 209 } 210 mSingleLineView.measure(singleLineWidthSpec, 211 MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST)); 212 maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight()); 213 } 214 int ownHeight = Math.min(maxChildHeight, maxSize); 215 setMeasuredDimension(width, ownHeight); 216 } 217 218 private boolean updateContractedHeaderWidth() { 219 // We need to update the expanded and the collapsed header to have exactly the same with to 220 // have the expand buttons laid out at the same location. 221 NotificationHeaderView contractedHeader = mContractedWrapper.getNotificationHeader(); 222 if (contractedHeader != null) { 223 if (mExpandedChild != null 224 && mExpandedWrapper.getNotificationHeader() != null) { 225 NotificationHeaderView expandedHeader = mExpandedWrapper.getNotificationHeader(); 226 int expandedSize = expandedHeader.getMeasuredWidth() 227 - expandedHeader.getPaddingEnd(); 228 int collapsedSize = contractedHeader.getMeasuredWidth() 229 - expandedHeader.getPaddingEnd(); 230 if (expandedSize != collapsedSize) { 231 int paddingEnd = contractedHeader.getMeasuredWidth() - expandedSize; 232 contractedHeader.setPadding( 233 contractedHeader.isLayoutRtl() 234 ? paddingEnd 235 : contractedHeader.getPaddingLeft(), 236 contractedHeader.getPaddingTop(), 237 contractedHeader.isLayoutRtl() 238 ? contractedHeader.getPaddingLeft() 239 : paddingEnd, 240 contractedHeader.getPaddingBottom()); 241 contractedHeader.setShowWorkBadgeAtEnd(true); 242 return true; 243 } 244 } else { 245 int paddingEnd = mNotificationContentMarginEnd; 246 if (contractedHeader.getPaddingEnd() != paddingEnd) { 247 contractedHeader.setPadding( 248 contractedHeader.isLayoutRtl() 249 ? paddingEnd 250 : contractedHeader.getPaddingLeft(), 251 contractedHeader.getPaddingTop(), 252 contractedHeader.isLayoutRtl() 253 ? contractedHeader.getPaddingLeft() 254 : paddingEnd, 255 contractedHeader.getPaddingBottom()); 256 contractedHeader.setShowWorkBadgeAtEnd(false); 257 return true; 258 } 259 } 260 } 261 return false; 262 } 263 264 private boolean shouldContractedBeFixedSize() { 265 return mBeforeN && mContractedWrapper instanceof NotificationCustomViewWrapper; 266 } 267 268 @Override 269 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 270 int previousHeight = 0; 271 if (mExpandedChild != null) { 272 previousHeight = mExpandedChild.getHeight(); 273 } 274 super.onLayout(changed, left, top, right, bottom); 275 if (previousHeight != 0 && mExpandedChild.getHeight() != previousHeight) { 276 mContentHeightAtAnimationStart = previousHeight; 277 } 278 updateClipping(); 279 invalidateOutline(); 280 selectLayout(false /* animate */, mForceSelectNextLayout /* force */); 281 mForceSelectNextLayout = false; 282 updateExpandButtons(mExpandable); 283 } 284 285 @Override 286 protected void onAttachedToWindow() { 287 super.onAttachedToWindow(); 288 updateVisibility(); 289 } 290 291 public void reset() { 292 if (mContractedChild != null) { 293 mContractedChild.animate().cancel(); 294 removeView(mContractedChild); 295 } 296 mPreviousExpandedRemoteInputIntent = null; 297 if (mExpandedRemoteInput != null) { 298 mExpandedRemoteInput.onNotificationUpdateOrReset(); 299 if (mExpandedRemoteInput.isActive()) { 300 mPreviousExpandedRemoteInputIntent = mExpandedRemoteInput.getPendingIntent(); 301 } 302 } 303 if (mExpandedChild != null) { 304 mExpandedChild.animate().cancel(); 305 removeView(mExpandedChild); 306 mExpandedRemoteInput = null; 307 } 308 mPreviousHeadsUpRemoteInputIntent = null; 309 if (mHeadsUpRemoteInput != null) { 310 mHeadsUpRemoteInput.onNotificationUpdateOrReset(); 311 if (mHeadsUpRemoteInput.isActive()) { 312 mPreviousHeadsUpRemoteInputIntent = mHeadsUpRemoteInput.getPendingIntent(); 313 } 314 } 315 if (mHeadsUpChild != null) { 316 mHeadsUpChild.animate().cancel(); 317 removeView(mHeadsUpChild); 318 mHeadsUpRemoteInput = null; 319 } 320 mContractedChild = null; 321 mExpandedChild = null; 322 mHeadsUpChild = null; 323 } 324 325 public View getContractedChild() { 326 return mContractedChild; 327 } 328 329 public View getExpandedChild() { 330 return mExpandedChild; 331 } 332 333 public View getHeadsUpChild() { 334 return mHeadsUpChild; 335 } 336 337 public void setContractedChild(View child) { 338 if (mContractedChild != null) { 339 mContractedChild.animate().cancel(); 340 removeView(mContractedChild); 341 } 342 addView(child); 343 mContractedChild = child; 344 mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child, 345 mContainingNotification); 346 mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */); 347 } 348 349 public void setExpandedChild(View child) { 350 if (mExpandedChild != null) { 351 mExpandedChild.animate().cancel(); 352 removeView(mExpandedChild); 353 } 354 addView(child); 355 mExpandedChild = child; 356 mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child, 357 mContainingNotification); 358 } 359 360 public void setHeadsUpChild(View child) { 361 if (mHeadsUpChild != null) { 362 mHeadsUpChild.animate().cancel(); 363 removeView(mHeadsUpChild); 364 } 365 addView(child); 366 mHeadsUpChild = child; 367 mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, 368 mContainingNotification); 369 } 370 371 @Override 372 protected void onVisibilityChanged(View changedView, int visibility) { 373 super.onVisibilityChanged(changedView, visibility); 374 updateVisibility(); 375 } 376 377 private void updateVisibility() { 378 setVisible(isShown()); 379 } 380 381 @Override 382 protected void onDetachedFromWindow() { 383 super.onDetachedFromWindow(); 384 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 385 } 386 387 private void setVisible(final boolean isVisible) { 388 if (isVisible) { 389 // This call can happen multiple times, but removing only removes a single one. 390 // We therefore need to remove the old one. 391 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 392 // We only animate if we are drawn at least once, otherwise the view might animate when 393 // it's shown the first time 394 getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener); 395 } else { 396 getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener); 397 mAnimate = false; 398 } 399 } 400 401 private void focusExpandButtonIfNecessary() { 402 if (mFocusOnVisibilityChange) { 403 NotificationHeaderView header = getVisibleNotificationHeader(); 404 if (header != null) { 405 ImageView expandButton = header.getExpandButton(); 406 if (expandButton != null) { 407 expandButton.requestAccessibilityFocus(); 408 } 409 } 410 mFocusOnVisibilityChange = false; 411 } 412 } 413 414 public void setContentHeight(int contentHeight) { 415 mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight()); 416 selectLayout(mAnimate /* animate */, false /* force */); 417 418 int minHeightHint = getMinContentHeightHint(); 419 420 NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); 421 if (wrapper != null) { 422 wrapper.setContentHeight(mContentHeight, minHeightHint); 423 } 424 425 wrapper = getVisibleWrapper(mTransformationStartVisibleType); 426 if (wrapper != null) { 427 wrapper.setContentHeight(mContentHeight, minHeightHint); 428 } 429 430 updateClipping(); 431 invalidateOutline(); 432 } 433 434 /** 435 * @return the minimum apparent height that the wrapper should allow for the purpose 436 * of aligning elements at the bottom edge. If this is larger than the content 437 * height, the notification is clipped instead of being further shrunk. 438 */ 439 private int getMinContentHeightHint() { 440 if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) { 441 return mContext.getResources().getDimensionPixelSize( 442 com.android.internal.R.dimen.notification_action_list_height); 443 } 444 445 // Transition between heads-up & expanded, or pinned. 446 if (mHeadsUpChild != null && mExpandedChild != null) { 447 boolean transitioningBetweenHunAndExpanded = 448 isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) || 449 isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP); 450 boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED) 451 && (mIsHeadsUp || mHeadsupDisappearRunning); 452 if (transitioningBetweenHunAndExpanded || pinned) { 453 return Math.min(mHeadsUpChild.getHeight(), mExpandedChild.getHeight()); 454 } 455 } 456 457 // Size change of the expanded version 458 if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart >= 0 459 && mExpandedChild != null) { 460 return Math.min(mContentHeightAtAnimationStart, mExpandedChild.getHeight()); 461 } 462 463 int hint; 464 if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) { 465 hint = mHeadsUpChild.getHeight(); 466 } else if (mExpandedChild != null) { 467 hint = mExpandedChild.getHeight(); 468 } else { 469 hint = mContractedChild.getHeight() + mContext.getResources().getDimensionPixelSize( 470 com.android.internal.R.dimen.notification_action_list_height); 471 } 472 473 if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) { 474 hint = Math.min(hint, mExpandedChild.getHeight()); 475 } 476 return hint; 477 } 478 479 private boolean isTransitioningFromTo(int from, int to) { 480 return (mTransformationStartVisibleType == from || mAnimationStartVisibleType == from) 481 && mVisibleType == to; 482 } 483 484 private boolean isVisibleOrTransitioning(int type) { 485 return mVisibleType == type || mTransformationStartVisibleType == type 486 || mAnimationStartVisibleType == type; 487 } 488 489 private void updateContentTransformation() { 490 int visibleType = calculateVisibleType(); 491 if (visibleType != mVisibleType) { 492 // A new transformation starts 493 mTransformationStartVisibleType = mVisibleType; 494 final TransformableView shownView = getTransformableViewForVisibleType(visibleType); 495 final TransformableView hiddenView = getTransformableViewForVisibleType( 496 mTransformationStartVisibleType); 497 shownView.transformFrom(hiddenView, 0.0f); 498 getViewForVisibleType(visibleType).setVisibility(View.VISIBLE); 499 hiddenView.transformTo(shownView, 0.0f); 500 mVisibleType = visibleType; 501 updateBackgroundColor(true /* animate */); 502 } 503 if (mForceSelectNextLayout) { 504 forceUpdateVisibilities(); 505 } 506 if (mTransformationStartVisibleType != UNDEFINED 507 && mVisibleType != mTransformationStartVisibleType 508 && getViewForVisibleType(mTransformationStartVisibleType) != null) { 509 final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType); 510 final TransformableView hiddenView = getTransformableViewForVisibleType( 511 mTransformationStartVisibleType); 512 float transformationAmount = calculateTransformationAmount(); 513 shownView.transformFrom(hiddenView, transformationAmount); 514 hiddenView.transformTo(shownView, transformationAmount); 515 updateBackgroundTransformation(transformationAmount); 516 } else { 517 updateViewVisibilities(visibleType); 518 updateBackgroundColor(false); 519 } 520 } 521 522 private void updateBackgroundTransformation(float transformationAmount) { 523 int endColor = getBackgroundColor(mVisibleType); 524 int startColor = getBackgroundColor(mTransformationStartVisibleType); 525 if (endColor != startColor) { 526 if (startColor == 0) { 527 startColor = mContainingNotification.getBackgroundColorWithoutTint(); 528 } 529 if (endColor == 0) { 530 endColor = mContainingNotification.getBackgroundColorWithoutTint(); 531 } 532 endColor = NotificationUtils.interpolateColors(startColor, endColor, 533 transformationAmount); 534 } 535 mContainingNotification.updateBackgroundAlpha(transformationAmount); 536 mContainingNotification.setContentBackground(endColor, false, this); 537 } 538 539 private float calculateTransformationAmount() { 540 int startHeight = getViewForVisibleType(mTransformationStartVisibleType).getHeight(); 541 int endHeight = getViewForVisibleType(mVisibleType).getHeight(); 542 int progress = Math.abs(mContentHeight - startHeight); 543 int totalDistance = Math.abs(endHeight - startHeight); 544 float amount = (float) progress / (float) totalDistance; 545 return Math.min(1.0f, amount); 546 } 547 548 public int getContentHeight() { 549 return mContentHeight; 550 } 551 552 public int getMaxHeight() { 553 if (mExpandedChild != null) { 554 return mExpandedChild.getHeight(); 555 } else if (mIsHeadsUp && mHeadsUpChild != null) { 556 return mHeadsUpChild.getHeight(); 557 } 558 return mContractedChild.getHeight(); 559 } 560 561 public int getMinHeight() { 562 return getMinHeight(false /* likeGroupExpanded */); 563 } 564 565 public int getMinHeight(boolean likeGroupExpanded) { 566 if (likeGroupExpanded || !mIsChildInGroup || isGroupExpanded()) { 567 return mContractedChild.getHeight(); 568 } else { 569 return mSingleLineView.getHeight(); 570 } 571 } 572 573 private boolean isGroupExpanded() { 574 return mGroupManager.isGroupExpanded(mStatusBarNotification); 575 } 576 577 public void setClipTopAmount(int clipTopAmount) { 578 mClipTopAmount = clipTopAmount; 579 updateClipping(); 580 } 581 582 private void updateClipping() { 583 if (mClipToActualHeight) { 584 mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight); 585 setClipBounds(mClipBounds); 586 } else { 587 setClipBounds(null); 588 } 589 } 590 591 public void setClipToActualHeight(boolean clipToActualHeight) { 592 mClipToActualHeight = clipToActualHeight; 593 updateClipping(); 594 } 595 596 private void selectLayout(boolean animate, boolean force) { 597 if (mContractedChild == null) { 598 return; 599 } 600 if (mUserExpanding) { 601 updateContentTransformation(); 602 } else { 603 int visibleType = calculateVisibleType(); 604 boolean changedType = visibleType != mVisibleType; 605 if (changedType || force) { 606 View visibleView = getViewForVisibleType(visibleType); 607 if (visibleView != null) { 608 visibleView.setVisibility(VISIBLE); 609 transferRemoteInputFocus(visibleType); 610 } 611 NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType); 612 if (visibleWrapper != null) { 613 visibleWrapper.setContentHeight(mContentHeight, getMinContentHeightHint()); 614 } 615 616 if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null) 617 || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null) 618 || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null) 619 || visibleType == VISIBLE_TYPE_CONTRACTED)) { 620 animateToVisibleType(visibleType); 621 } else { 622 updateViewVisibilities(visibleType); 623 } 624 mVisibleType = visibleType; 625 if (changedType) { 626 focusExpandButtonIfNecessary(); 627 } 628 updateBackgroundColor(animate); 629 } 630 } 631 } 632 633 private void forceUpdateVisibilities() { 634 boolean contractedVisible = mVisibleType == VISIBLE_TYPE_CONTRACTED 635 || mTransformationStartVisibleType == VISIBLE_TYPE_CONTRACTED; 636 boolean expandedVisible = mVisibleType == VISIBLE_TYPE_EXPANDED 637 || mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED; 638 boolean headsUpVisible = mVisibleType == VISIBLE_TYPE_HEADSUP 639 || mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP; 640 boolean singleLineVisible = mVisibleType == VISIBLE_TYPE_SINGLELINE 641 || mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE; 642 if (!contractedVisible) { 643 mContractedChild.setVisibility(View.INVISIBLE); 644 } else { 645 mContractedWrapper.setVisible(true); 646 } 647 if (mExpandedChild != null) { 648 if (!expandedVisible) { 649 mExpandedChild.setVisibility(View.INVISIBLE); 650 } else { 651 mExpandedWrapper.setVisible(true); 652 } 653 } 654 if (mHeadsUpChild != null) { 655 if (!headsUpVisible) { 656 mHeadsUpChild.setVisibility(View.INVISIBLE); 657 } else { 658 mHeadsUpWrapper.setVisible(true); 659 } 660 } 661 if (mSingleLineView != null) { 662 if (!singleLineVisible) { 663 mSingleLineView.setVisibility(View.INVISIBLE); 664 } else { 665 mSingleLineView.setVisible(true); 666 } 667 } 668 } 669 670 public void updateBackgroundColor(boolean animate) { 671 int customBackgroundColor = getBackgroundColor(mVisibleType); 672 mContainingNotification.resetBackgroundAlpha(); 673 mContainingNotification.setContentBackground(customBackgroundColor, animate, this); 674 } 675 676 public int getVisibleType() { 677 return mVisibleType; 678 } 679 680 public int getBackgroundColorForExpansionState() { 681 // When expanding or user locked we want the new type, when collapsing we want 682 // the original type 683 final int visibleType = (mContainingNotification.isGroupExpanded() 684 || mContainingNotification.isUserLocked()) 685 ? calculateVisibleType() 686 : getVisibleType(); 687 return getBackgroundColor(visibleType); 688 } 689 690 public int getBackgroundColor(int visibleType) { 691 NotificationViewWrapper currentVisibleWrapper = getVisibleWrapper(visibleType); 692 int customBackgroundColor = 0; 693 if (currentVisibleWrapper != null) { 694 customBackgroundColor = currentVisibleWrapper.getCustomBackgroundColor(); 695 } 696 return customBackgroundColor; 697 } 698 699 private void updateViewVisibilities(int visibleType) { 700 boolean contractedVisible = visibleType == VISIBLE_TYPE_CONTRACTED; 701 mContractedWrapper.setVisible(contractedVisible); 702 if (mExpandedChild != null) { 703 boolean expandedVisible = visibleType == VISIBLE_TYPE_EXPANDED; 704 mExpandedWrapper.setVisible(expandedVisible); 705 } 706 if (mHeadsUpChild != null) { 707 boolean headsUpVisible = visibleType == VISIBLE_TYPE_HEADSUP; 708 mHeadsUpWrapper.setVisible(headsUpVisible); 709 } 710 if (mSingleLineView != null) { 711 boolean singleLineVisible = visibleType == VISIBLE_TYPE_SINGLELINE; 712 mSingleLineView.setVisible(singleLineVisible); 713 } 714 } 715 716 private void animateToVisibleType(int visibleType) { 717 final TransformableView shownView = getTransformableViewForVisibleType(visibleType); 718 final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType); 719 if (shownView == hiddenView || hiddenView == null) { 720 shownView.setVisible(true); 721 return; 722 } 723 mAnimationStartVisibleType = mVisibleType; 724 shownView.transformFrom(hiddenView); 725 getViewForVisibleType(visibleType).setVisibility(View.VISIBLE); 726 hiddenView.transformTo(shownView, new Runnable() { 727 @Override 728 public void run() { 729 if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) { 730 hiddenView.setVisible(false); 731 } 732 mAnimationStartVisibleType = UNDEFINED; 733 } 734 }); 735 } 736 737 private void transferRemoteInputFocus(int visibleType) { 738 if (visibleType == VISIBLE_TYPE_HEADSUP 739 && mHeadsUpRemoteInput != null 740 && (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive())) { 741 mHeadsUpRemoteInput.stealFocusFrom(mExpandedRemoteInput); 742 } 743 if (visibleType == VISIBLE_TYPE_EXPANDED 744 && mExpandedRemoteInput != null 745 && (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive())) { 746 mExpandedRemoteInput.stealFocusFrom(mHeadsUpRemoteInput); 747 } 748 } 749 750 /** 751 * @param visibleType one of the static enum types in this view 752 * @return the corresponding transformable view according to the given visible type 753 */ 754 private TransformableView getTransformableViewForVisibleType(int visibleType) { 755 switch (visibleType) { 756 case VISIBLE_TYPE_EXPANDED: 757 return mExpandedWrapper; 758 case VISIBLE_TYPE_HEADSUP: 759 return mHeadsUpWrapper; 760 case VISIBLE_TYPE_SINGLELINE: 761 return mSingleLineView; 762 default: 763 return mContractedWrapper; 764 } 765 } 766 767 /** 768 * @param visibleType one of the static enum types in this view 769 * @return the corresponding view according to the given visible type 770 */ 771 private View getViewForVisibleType(int visibleType) { 772 switch (visibleType) { 773 case VISIBLE_TYPE_EXPANDED: 774 return mExpandedChild; 775 case VISIBLE_TYPE_HEADSUP: 776 return mHeadsUpChild; 777 case VISIBLE_TYPE_SINGLELINE: 778 return mSingleLineView; 779 default: 780 return mContractedChild; 781 } 782 } 783 784 private NotificationViewWrapper getVisibleWrapper(int visibleType) { 785 switch (visibleType) { 786 case VISIBLE_TYPE_EXPANDED: 787 return mExpandedWrapper; 788 case VISIBLE_TYPE_HEADSUP: 789 return mHeadsUpWrapper; 790 case VISIBLE_TYPE_CONTRACTED: 791 return mContractedWrapper; 792 default: 793 return null; 794 } 795 } 796 797 /** 798 * @return one of the static enum types in this view, calculated form the current state 799 */ 800 public int calculateVisibleType() { 801 if (mUserExpanding) { 802 int height = !mIsChildInGroup || isGroupExpanded() 803 || mContainingNotification.isExpanded(true /* allowOnKeyguard */) 804 ? mContainingNotification.getMaxContentHeight() 805 : mContainingNotification.getShowingLayout().getMinHeight(); 806 if (height == 0) { 807 height = mContentHeight; 808 } 809 int expandedVisualType = getVisualTypeForHeight(height); 810 int collapsedVisualType = mIsChildInGroup && !isGroupExpanded() 811 ? VISIBLE_TYPE_SINGLELINE 812 : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight()); 813 return mTransformationStartVisibleType == collapsedVisualType 814 ? expandedVisualType 815 : collapsedVisualType; 816 } 817 int intrinsicHeight = mContainingNotification.getIntrinsicHeight(); 818 int viewHeight = mContentHeight; 819 if (intrinsicHeight != 0) { 820 // the intrinsicHeight might be 0 because it was just reset. 821 viewHeight = Math.min(mContentHeight, intrinsicHeight); 822 } 823 return getVisualTypeForHeight(viewHeight); 824 } 825 826 private int getVisualTypeForHeight(float viewHeight) { 827 boolean noExpandedChild = mExpandedChild == null; 828 if (!noExpandedChild && viewHeight == mExpandedChild.getHeight()) { 829 return VISIBLE_TYPE_EXPANDED; 830 } 831 if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) { 832 return VISIBLE_TYPE_SINGLELINE; 833 } 834 835 if ((mIsHeadsUp || mHeadsupDisappearRunning) && mHeadsUpChild != null) { 836 if (viewHeight <= mHeadsUpChild.getHeight() || noExpandedChild) { 837 return VISIBLE_TYPE_HEADSUP; 838 } else { 839 return VISIBLE_TYPE_EXPANDED; 840 } 841 } else { 842 if (noExpandedChild || (viewHeight <= mContractedChild.getHeight() 843 && (!mIsChildInGroup || isGroupExpanded() 844 || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) { 845 return VISIBLE_TYPE_CONTRACTED; 846 } else { 847 return VISIBLE_TYPE_EXPANDED; 848 } 849 } 850 } 851 852 public boolean isContentExpandable() { 853 return mExpandedChild != null; 854 } 855 856 public void setDark(boolean dark, boolean fade, long delay) { 857 if (mContractedChild == null) { 858 return; 859 } 860 mDark = dark; 861 if (mVisibleType == VISIBLE_TYPE_CONTRACTED || !dark) { 862 mContractedWrapper.setDark(dark, fade, delay); 863 } 864 if (mVisibleType == VISIBLE_TYPE_EXPANDED || (mExpandedChild != null && !dark)) { 865 mExpandedWrapper.setDark(dark, fade, delay); 866 } 867 if (mVisibleType == VISIBLE_TYPE_HEADSUP || (mHeadsUpChild != null && !dark)) { 868 mHeadsUpWrapper.setDark(dark, fade, delay); 869 } 870 if (mSingleLineView != null && (mVisibleType == VISIBLE_TYPE_SINGLELINE || !dark)) { 871 mSingleLineView.setDark(dark, fade, delay); 872 } 873 } 874 875 public void setHeadsUp(boolean headsUp) { 876 mIsHeadsUp = headsUp; 877 selectLayout(false /* animate */, true /* force */); 878 updateExpandButtons(mExpandable); 879 } 880 881 @Override 882 public boolean hasOverlappingRendering() { 883 884 // This is not really true, but good enough when fading from the contracted to the expanded 885 // layout, and saves us some layers. 886 return false; 887 } 888 889 public void setShowingLegacyBackground(boolean showing) { 890 mShowingLegacyBackground = showing; 891 updateShowingLegacyBackground(); 892 } 893 894 private void updateShowingLegacyBackground() { 895 if (mContractedChild != null) { 896 mContractedWrapper.setShowingLegacyBackground(mShowingLegacyBackground); 897 } 898 if (mExpandedChild != null) { 899 mExpandedWrapper.setShowingLegacyBackground(mShowingLegacyBackground); 900 } 901 if (mHeadsUpChild != null) { 902 mHeadsUpWrapper.setShowingLegacyBackground(mShowingLegacyBackground); 903 } 904 } 905 906 public void setIsChildInGroup(boolean isChildInGroup) { 907 mIsChildInGroup = isChildInGroup; 908 updateSingleLineView(); 909 } 910 911 public void onNotificationUpdated(NotificationData.Entry entry) { 912 mStatusBarNotification = entry.notification; 913 mBeforeN = entry.targetSdk < Build.VERSION_CODES.N; 914 updateSingleLineView(); 915 applyRemoteInput(entry); 916 if (mContractedChild != null) { 917 mContractedWrapper.notifyContentUpdated(entry.notification); 918 } 919 if (mExpandedChild != null) { 920 mExpandedWrapper.notifyContentUpdated(entry.notification); 921 } 922 if (mHeadsUpChild != null) { 923 mHeadsUpWrapper.notifyContentUpdated(entry.notification); 924 } 925 updateShowingLegacyBackground(); 926 mForceSelectNextLayout = true; 927 setDark(mDark, false /* animate */, 0 /* delay */); 928 mPreviousExpandedRemoteInputIntent = null; 929 mPreviousHeadsUpRemoteInputIntent = null; 930 } 931 932 private void updateSingleLineView() { 933 if (mIsChildInGroup) { 934 mSingleLineView = mHybridGroupManager.bindFromNotification( 935 mSingleLineView, mStatusBarNotification.getNotification()); 936 } else if (mSingleLineView != null) { 937 removeView(mSingleLineView); 938 mSingleLineView = null; 939 } 940 } 941 942 private void applyRemoteInput(final NotificationData.Entry entry) { 943 if (mRemoteInputController == null) { 944 return; 945 } 946 947 boolean hasRemoteInput = false; 948 949 Notification.Action[] actions = entry.notification.getNotification().actions; 950 if (actions != null) { 951 for (Notification.Action a : actions) { 952 if (a.getRemoteInputs() != null) { 953 for (RemoteInput ri : a.getRemoteInputs()) { 954 if (ri.getAllowFreeFormInput()) { 955 hasRemoteInput = true; 956 break; 957 } 958 } 959 } 960 } 961 } 962 963 View bigContentView = mExpandedChild; 964 if (bigContentView != null) { 965 mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasRemoteInput, 966 mPreviousExpandedRemoteInputIntent); 967 } else { 968 mExpandedRemoteInput = null; 969 } 970 971 View headsUpContentView = mHeadsUpChild; 972 if (headsUpContentView != null) { 973 mHeadsUpRemoteInput = applyRemoteInput(headsUpContentView, entry, hasRemoteInput, 974 mPreviousHeadsUpRemoteInputIntent); 975 } else { 976 mHeadsUpRemoteInput = null; 977 } 978 } 979 980 private RemoteInputView applyRemoteInput(View view, NotificationData.Entry entry, 981 boolean hasRemoteInput, PendingIntent existingPendingIntent) { 982 View actionContainerCandidate = view.findViewById( 983 com.android.internal.R.id.actions_container); 984 if (actionContainerCandidate instanceof FrameLayout) { 985 RemoteInputView existing = (RemoteInputView) 986 view.findViewWithTag(RemoteInputView.VIEW_TAG); 987 988 if (existing != null) { 989 existing.onNotificationUpdateOrReset(); 990 } 991 992 if (existing == null && hasRemoteInput) { 993 ViewGroup actionContainer = (FrameLayout) actionContainerCandidate; 994 RemoteInputView riv = RemoteInputView.inflate( 995 mContext, actionContainer, entry, mRemoteInputController); 996 997 riv.setVisibility(View.INVISIBLE); 998 actionContainer.addView(riv, new LayoutParams( 999 ViewGroup.LayoutParams.MATCH_PARENT, 1000 ViewGroup.LayoutParams.MATCH_PARENT) 1001 ); 1002 existing = riv; 1003 } 1004 if (hasRemoteInput) { 1005 int color = entry.notification.getNotification().color; 1006 if (color == Notification.COLOR_DEFAULT) { 1007 color = mContext.getColor(R.color.default_remote_input_background); 1008 } 1009 existing.setBackgroundColor(NotificationColorUtil.ensureTextBackgroundColor(color, 1010 mContext.getColor(R.color.remote_input_text_enabled), 1011 mContext.getColor(R.color.remote_input_hint))); 1012 1013 if (existingPendingIntent != null || existing.isActive()) { 1014 // The current action could be gone, or the pending intent no longer valid. 1015 // If we find a matching action in the new notification, focus, otherwise close. 1016 Notification.Action[] actions = entry.notification.getNotification().actions; 1017 if (existingPendingIntent != null) { 1018 existing.setPendingIntent(existingPendingIntent); 1019 } 1020 if (existing.updatePendingIntentFromActions(actions)) { 1021 if (!existing.isActive()) { 1022 existing.focus(); 1023 } 1024 } else { 1025 if (existing.isActive()) { 1026 existing.close(); 1027 } 1028 } 1029 } 1030 } 1031 return existing; 1032 } 1033 return null; 1034 } 1035 1036 public void closeRemoteInput() { 1037 if (mHeadsUpRemoteInput != null) { 1038 mHeadsUpRemoteInput.close(); 1039 } 1040 if (mExpandedRemoteInput != null) { 1041 mExpandedRemoteInput.close(); 1042 } 1043 } 1044 1045 public void setGroupManager(NotificationGroupManager groupManager) { 1046 mGroupManager = groupManager; 1047 } 1048 1049 public void setRemoteInputController(RemoteInputController r) { 1050 mRemoteInputController = r; 1051 } 1052 1053 public void setExpandClickListener(OnClickListener expandClickListener) { 1054 mExpandClickListener = expandClickListener; 1055 } 1056 1057 public void updateExpandButtons(boolean expandable) { 1058 mExpandable = expandable; 1059 // if the expanded child has the same height as the collapsed one we hide it. 1060 if (mExpandedChild != null && mExpandedChild.getHeight() != 0) { 1061 if ((!mIsHeadsUp || mHeadsUpChild == null)) { 1062 if (mExpandedChild.getHeight() == mContractedChild.getHeight()) { 1063 expandable = false; 1064 } 1065 } else if (mExpandedChild.getHeight() == mHeadsUpChild.getHeight()) { 1066 expandable = false; 1067 } 1068 } 1069 if (mExpandedChild != null) { 1070 mExpandedWrapper.updateExpandability(expandable, mExpandClickListener); 1071 } 1072 if (mContractedChild != null) { 1073 mContractedWrapper.updateExpandability(expandable, mExpandClickListener); 1074 } 1075 if (mHeadsUpChild != null) { 1076 mHeadsUpWrapper.updateExpandability(expandable, mExpandClickListener); 1077 } 1078 } 1079 1080 public NotificationHeaderView getNotificationHeader() { 1081 NotificationHeaderView header = null; 1082 if (mContractedChild != null) { 1083 header = mContractedWrapper.getNotificationHeader(); 1084 } 1085 if (header == null && mExpandedChild != null) { 1086 header = mExpandedWrapper.getNotificationHeader(); 1087 } 1088 if (header == null && mHeadsUpChild != null) { 1089 header = mHeadsUpWrapper.getNotificationHeader(); 1090 } 1091 return header; 1092 } 1093 1094 public NotificationHeaderView getVisibleNotificationHeader() { 1095 NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); 1096 return wrapper == null ? null : wrapper.getNotificationHeader(); 1097 } 1098 1099 public void setContainingNotification(ExpandableNotificationRow containingNotification) { 1100 mContainingNotification = containingNotification; 1101 } 1102 1103 public void requestSelectLayout(boolean needsAnimation) { 1104 selectLayout(needsAnimation, false); 1105 } 1106 1107 public void reInflateViews() { 1108 if (mIsChildInGroup && mSingleLineView != null) { 1109 removeView(mSingleLineView); 1110 mSingleLineView = null; 1111 updateSingleLineView(); 1112 } 1113 } 1114 1115 public void setUserExpanding(boolean userExpanding) { 1116 mUserExpanding = userExpanding; 1117 if (userExpanding) { 1118 mTransformationStartVisibleType = mVisibleType; 1119 } else { 1120 mTransformationStartVisibleType = UNDEFINED; 1121 mVisibleType = calculateVisibleType(); 1122 updateViewVisibilities(mVisibleType); 1123 updateBackgroundColor(false); 1124 } 1125 } 1126 1127 /** 1128 * Set by how much the single line view should be indented. Used when a overflow indicator is 1129 * present and only during measuring 1130 */ 1131 public void setSingleLineWidthIndention(int singleLineWidthIndention) { 1132 if (singleLineWidthIndention != mSingleLineWidthIndention) { 1133 mSingleLineWidthIndention = singleLineWidthIndention; 1134 mContainingNotification.forceLayout(); 1135 forceLayout(); 1136 } 1137 } 1138 1139 public HybridNotificationView getSingleLineView() { 1140 return mSingleLineView; 1141 } 1142 1143 public void setRemoved() { 1144 if (mExpandedRemoteInput != null) { 1145 mExpandedRemoteInput.setRemoved(); 1146 } 1147 if (mHeadsUpRemoteInput != null) { 1148 mHeadsUpRemoteInput.setRemoved(); 1149 } 1150 } 1151 1152 public void setContentHeightAnimating(boolean animating) { 1153 if (!animating) { 1154 mContentHeightAtAnimationStart = UNDEFINED; 1155 } 1156 } 1157 1158 public void setHeadsupDisappearRunning(boolean headsupDisappearRunning) { 1159 mHeadsupDisappearRunning = headsupDisappearRunning; 1160 selectLayout(false /* animate */, true /* force */); 1161 } 1162 1163 public void setFocusOnVisibilityChange() { 1164 mFocusOnVisibilityChange = true; 1165 } 1166 } 1167