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