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