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.Canvas;
25 import android.graphics.Rect;
26 import android.graphics.drawable.Drawable;
27 import android.os.Build;
28 import android.os.RemoteException;
29 import android.os.Trace;
30 import android.service.notification.StatusBarNotification;
31 import android.util.ArrayMap;
32 import android.util.AttributeSet;
33 import android.util.IndentingPrintWriter;
34 import android.util.Log;
35 import android.view.LayoutInflater;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.view.ViewTreeObserver;
40 import android.view.accessibility.AccessibilityEvent;
41 import android.widget.FrameLayout;
42 import android.widget.ImageView;
43 import android.widget.LinearLayout;
44 
45 import androidx.annotation.MainThread;
46 
47 import com.android.internal.annotations.VisibleForTesting;
48 import com.android.internal.statusbar.IStatusBarService;
49 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
50 import com.android.systemui.res.R;
51 import com.android.systemui.statusbar.RemoteInputController;
52 import com.android.systemui.statusbar.SmartReplyController;
53 import com.android.systemui.statusbar.TransformableView;
54 import com.android.systemui.statusbar.notification.FeedbackIcon;
55 import com.android.systemui.statusbar.notification.NotificationFadeAware;
56 import com.android.systemui.statusbar.notification.NotificationUtils;
57 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
58 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
59 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
60 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
61 import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper;
62 import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper;
63 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
64 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
65 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
66 import com.android.systemui.statusbar.policy.RemoteInputView;
67 import com.android.systemui.statusbar.policy.RemoteInputViewController;
68 import com.android.systemui.statusbar.policy.SmartReplyConstants;
69 import com.android.systemui.statusbar.policy.SmartReplyStateInflaterKt;
70 import com.android.systemui.statusbar.policy.SmartReplyView;
71 import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent;
72 import com.android.systemui.util.Compile;
73 import com.android.systemui.util.DumpUtilsKt;
74 
75 import java.io.PrintWriter;
76 import java.util.ArrayList;
77 import java.util.Collections;
78 import java.util.List;
79 
80 /**
81  * A frame layout containing the actual payload of the notification, including the contracted,
82  * expanded and heads up layout. This class is responsible for clipping the content and
83  * switching between the expanded, contracted and the heads up view depending on its clipped size.
84  */
85 public class NotificationContentView extends FrameLayout implements NotificationFadeAware {
86 
87     private static final String TAG = "NotificationContentView";
88     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
89     public static final int VISIBLE_TYPE_CONTRACTED = 0;
90     public static final int VISIBLE_TYPE_EXPANDED = 1;
91     public static final int VISIBLE_TYPE_HEADSUP = 2;
92     public static final int VISIBLE_TYPE_SINGLELINE = 3;
93     /**
94      * Used when there is no content on the view such as when we're a public layout but don't
95      * need to show.
96      */
97     private static final int VISIBLE_TYPE_NONE = -1;
98 
99     private static final int UNDEFINED = -1;
100 
101     protected static final boolean INCLUDE_HEIGHTS_TO_DUMP = true;
102 
103     private final Rect mClipBounds = new Rect();
104 
105     private int mMinContractedHeight;
106     private int mMinSingleLineHeight;
107     private View mContractedChild;
108     private View mExpandedChild;
109     private View mHeadsUpChild;
110     private HybridNotificationView mSingleLineView;
111 
112     private RemoteInputView mExpandedRemoteInput;
113     private RemoteInputView mHeadsUpRemoteInput;
114 
115     private SmartReplyConstants mSmartReplyConstants;
116     private SmartReplyView mExpandedSmartReplyView;
117     private SmartReplyView mHeadsUpSmartReplyView;
118     @Nullable private RemoteInputViewController mExpandedRemoteInputController;
119     @Nullable private RemoteInputViewController mHeadsUpRemoteInputController;
120     private SmartReplyController mSmartReplyController;
121     private InflatedSmartReplyViewHolder mExpandedInflatedSmartReplies;
122     private InflatedSmartReplyViewHolder mHeadsUpInflatedSmartReplies;
123     private InflatedSmartReplyState mCurrentSmartReplyState;
124 
125     private NotificationViewWrapper mContractedWrapper;
126     private NotificationViewWrapper mExpandedWrapper;
127     private NotificationViewWrapper mHeadsUpWrapper;
128     @Nullable private NotificationViewWrapper mShownWrapper = null;
129     private final HybridGroupManager mHybridGroupManager;
130     private int mClipTopAmount;
131     private int mContentHeight;
132     private int mVisibleType = VISIBLE_TYPE_NONE;
133     private boolean mAnimate;
134     private boolean mIsHeadsUp;
135     private boolean mLegacy;
136     private boolean mIsChildInGroup;
137     private int mSmallHeight;
138     private int mHeadsUpHeight;
139     private int mNotificationMaxHeight;
140     private NotificationEntry mNotificationEntry;
141     private RemoteInputController mRemoteInputController;
142     private Runnable mExpandedVisibleListener;
143     private PeopleNotificationIdentifier mPeopleIdentifier;
144     private RemoteInputViewSubcomponent.Factory mRemoteInputSubcomponentFactory;
145     private IStatusBarService mStatusBarService;
146     private boolean mBubblesEnabledForUser;
147 
148     /**
149      * List of listeners for when content views become inactive (i.e. not the showing view).
150      */
151     private final ArrayMap<View, Runnable> mOnContentViewInactiveListeners = new ArrayMap<>();
152 
153     private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener
154             = new ViewTreeObserver.OnPreDrawListener() {
155         @Override
156         public boolean onPreDraw() {
157             // We need to post since we don't want the notification to animate on the very first
158             // frame
159             post(new Runnable() {
160                 @Override
161                 public void run() {
162                     mAnimate = true;
163                 }
164             });
165             getViewTreeObserver().removeOnPreDrawListener(this);
166             return true;
167         }
168     };
169 
170     private OnClickListener mExpandClickListener;
171     private boolean mBeforeN;
172     private boolean mExpandable;
173     private boolean mClipToActualHeight = true;
174     private ExpandableNotificationRow mContainingNotification;
175     /** The visible type at the start of a touch driven transformation */
176     private int mTransformationStartVisibleType;
177     /** The visible type at the start of an animation driven transformation */
178     private int mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
179     private boolean mUserExpanding;
180     private int mSingleLineWidthIndention;
181     private boolean mForceSelectNextLayout = true;
182 
183     // Cache for storing the RemoteInputView during a notification update. Needed because
184     // setExpandedChild sets the actual field to null, but then onNotificationUpdated will restore
185     // it from the cache, if present, otherwise inflate a new one.
186     // ONLY USED WHEN THE ORIGINAL WAS isActive() WHEN REPLACED
187     private RemoteInputView mCachedExpandedRemoteInput;
188     private RemoteInputView mCachedHeadsUpRemoteInput;
189     private RemoteInputViewController mCachedExpandedRemoteInputViewController;
190     private RemoteInputViewController mCachedHeadsUpRemoteInputViewController;
191     private PendingIntent mPreviousExpandedRemoteInputIntent;
192     private PendingIntent mPreviousHeadsUpRemoteInputIntent;
193 
194     private int mContentHeightAtAnimationStart = UNDEFINED;
195     private boolean mFocusOnVisibilityChange;
196     private boolean mHeadsUpAnimatingAway;
197     private int mClipBottomAmount;
198     private boolean mIsContentExpandable;
199     private boolean mRemoteInputVisible;
200     private int mUnrestrictedContentHeight;
201 
202     private boolean mContentAnimating;
203 
NotificationContentView(Context context, AttributeSet attrs)204     public NotificationContentView(Context context, AttributeSet attrs) {
205         super(context, attrs);
206         mHybridGroupManager = new HybridGroupManager(getContext());
207         reinflate();
208     }
209 
initialize( PeopleNotificationIdentifier peopleNotificationIdentifier, RemoteInputViewSubcomponent.Factory rivSubcomponentFactory, SmartReplyConstants smartReplyConstants, SmartReplyController smartReplyController, IStatusBarService statusBarService)210     public void initialize(
211             PeopleNotificationIdentifier peopleNotificationIdentifier,
212             RemoteInputViewSubcomponent.Factory rivSubcomponentFactory,
213             SmartReplyConstants smartReplyConstants,
214             SmartReplyController smartReplyController,
215             IStatusBarService statusBarService) {
216         mPeopleIdentifier = peopleNotificationIdentifier;
217         mRemoteInputSubcomponentFactory = rivSubcomponentFactory;
218         mSmartReplyConstants = smartReplyConstants;
219         mSmartReplyController = smartReplyController;
220         mStatusBarService = statusBarService;
221         // We set root namespace so that we avoid searching children for id. Notification  might
222         // contain custom view and their ids may clash with ids already existing in shade or
223         // notification panel
224         setIsRootNamespace(true);
225     }
226 
227     @Override
focusSearch(View focused, int direction)228     public View focusSearch(View focused, int direction) {
229         // This implementation is copied from ViewGroup but with removed special handling of
230         // setIsRootNamespace. This view is set as tree root using setIsRootNamespace and it
231         // causes focus to be stuck inside of it. We need to be root to avoid id conflicts
232         // but we don't want to behave like root when it comes to focusing.
233         if (mParent != null) {
234             return mParent.focusSearch(focused, direction);
235         }
236         Log.wtf(TAG, "NotificationContentView doesn't have parent");
237         return null;
238     }
239 
reinflate()240     public void reinflate() {
241         mMinContractedHeight = getResources().getDimensionPixelSize(
242                 R.dimen.min_notification_layout_height);
243         if (AsyncHybridViewInflation.isEnabled()) {
244             //TODO (b/217799515): single-line view height is the greater of two heights: text view
245             // height and icon height (when there's an icon). icon height is fixed to be
246             // conversation_single_line_face_pile_size (24dp), the text view's height is 16sp,
247             // its pixel height changes with the system's font scaling factor.
248             mMinSingleLineHeight = getResources().getDimensionPixelSize(
249                     R.dimen.conversation_single_line_face_pile_size);
250         }
251     }
252 
setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight)253     public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight) {
254         mSmallHeight = smallHeight;
255         mHeadsUpHeight = headsUpMaxHeight;
256         mNotificationMaxHeight = maxHeight;
257     }
258 
259     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)260     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
261         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
262         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
263         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
264         int maxSize = Integer.MAX_VALUE / 2;
265         int width = MeasureSpec.getSize(widthMeasureSpec);
266         if (hasFixedHeight || isHeightLimited) {
267             maxSize = MeasureSpec.getSize(heightMeasureSpec);
268         }
269         int maxChildHeight = 0;
270         if (mExpandedChild != null) {
271             int notificationMaxHeight = mNotificationMaxHeight;
272             if (mExpandedSmartReplyView != null) {
273                 notificationMaxHeight += mExpandedSmartReplyView.getHeightUpperLimit();
274             }
275             notificationMaxHeight += mExpandedWrapper.getExtraMeasureHeight();
276             int size = notificationMaxHeight;
277             ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams();
278             boolean useExactly = false;
279             if (layoutParams.height >= 0) {
280                 // An actual height is set
281                 size = Math.min(size, layoutParams.height);
282                 useExactly = true;
283             }
284             int spec = MeasureSpec.makeMeasureSpec(size, useExactly
285                             ? MeasureSpec.EXACTLY
286                             : MeasureSpec.AT_MOST);
287             measureChildWithMargins(mExpandedChild, widthMeasureSpec, 0, spec, 0);
288             maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight());
289         }
290         if (mContractedChild != null) {
291             int heightSpec;
292             int size = mSmallHeight;
293             ViewGroup.LayoutParams layoutParams = mContractedChild.getLayoutParams();
294             boolean useExactly = false;
295             if (layoutParams.height >= 0) {
296                 // An actual height is set
297                 size = Math.min(size, layoutParams.height);
298                 useExactly = true;
299             }
300             if (shouldContractedBeFixedSize() || useExactly) {
301                 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
302             } else {
303                 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
304             }
305             measureChildWithMargins(mContractedChild, widthMeasureSpec, 0, heightSpec, 0);
306             int measuredHeight = mContractedChild.getMeasuredHeight();
307             if (measuredHeight < mMinContractedHeight) {
308                 heightSpec = MeasureSpec.makeMeasureSpec(mMinContractedHeight, MeasureSpec.EXACTLY);
309                 measureChildWithMargins(mContractedChild, widthMeasureSpec, 0, heightSpec, 0);
310             }
311             maxChildHeight = Math.max(maxChildHeight, measuredHeight);
312             if (mExpandedChild != null
313                     && mContractedChild.getMeasuredHeight() > mExpandedChild.getMeasuredHeight()) {
314                 // the Expanded child is smaller then the collapsed. Let's remeasure it.
315                 heightSpec = MeasureSpec.makeMeasureSpec(mContractedChild.getMeasuredHeight(),
316                         MeasureSpec.EXACTLY);
317                 measureChildWithMargins(mExpandedChild, widthMeasureSpec, 0, heightSpec, 0);
318             }
319         }
320         if (mHeadsUpChild != null) {
321             int maxHeight = mHeadsUpHeight;
322             if (mHeadsUpSmartReplyView != null) {
323                 maxHeight += mHeadsUpSmartReplyView.getHeightUpperLimit();
324             }
325             maxHeight += mHeadsUpWrapper.getExtraMeasureHeight();
326             int size = maxHeight;
327             ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams();
328             boolean useExactly = false;
329             if (layoutParams.height >= 0) {
330                 // An actual height is set
331                 size = Math.min(size, layoutParams.height);
332                 useExactly = true;
333             }
334             measureChildWithMargins(mHeadsUpChild, widthMeasureSpec, 0,
335                     MeasureSpec.makeMeasureSpec(size, useExactly ? MeasureSpec.EXACTLY
336                             : MeasureSpec.AT_MOST), 0);
337             maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight());
338         }
339         if (mSingleLineView != null) {
340             int singleLineWidthSpec = widthMeasureSpec;
341             if (mSingleLineWidthIndention != 0
342                     && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
343                 singleLineWidthSpec = MeasureSpec.makeMeasureSpec(
344                         width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(),
345                         MeasureSpec.EXACTLY);
346             }
347             mSingleLineView.measure(singleLineWidthSpec,
348                     MeasureSpec.makeMeasureSpec(mNotificationMaxHeight, MeasureSpec.AT_MOST));
349             maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight());
350         }
351         int ownHeight = Math.min(maxChildHeight, maxSize);
352         setMeasuredDimension(width, ownHeight);
353     }
354 
355     /**
356      * Get the extra height that needs to be added to the notification height for a given
357      * {@link RemoteInputView}.
358      * This is needed when the user is inline replying in order to ensure that the reply bar has
359      * enough padding.
360      *
361      * @param remoteInput The remote input to check.
362      * @return The extra height needed.
363      */
getExtraRemoteInputHeight(RemoteInputView remoteInput)364     private int getExtraRemoteInputHeight(RemoteInputView remoteInput) {
365         if (remoteInput != null && (remoteInput.isActive() || remoteInput.isSending())) {
366             return getResources().getDimensionPixelSize(
367                     com.android.internal.R.dimen.notification_content_margin);
368         }
369         return 0;
370     }
371 
shouldContractedBeFixedSize()372     private boolean shouldContractedBeFixedSize() {
373         return mBeforeN && mContractedWrapper instanceof NotificationCustomViewWrapper;
374     }
375 
376     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)377     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
378         int previousHeight = 0;
379         if (mExpandedChild != null) {
380             previousHeight = mExpandedChild.getHeight();
381         }
382         super.onLayout(changed, left, top, right, bottom);
383         if (previousHeight != 0 && mExpandedChild.getHeight() != previousHeight) {
384             mContentHeightAtAnimationStart = previousHeight;
385         }
386         updateClipping();
387         invalidateOutline();
388         selectLayout(false /* animate */, mForceSelectNextLayout /* force */);
389         mForceSelectNextLayout = false;
390         // TODO(b/182314698): move this to onMeasure.  This requires switching to getMeasuredHeight,
391         //  and also requires revisiting all of the logic called earlier in this method.
392         updateExpandButtonsDuringLayout(mExpandable, true /* duringLayout */);
393     }
394 
395     @Override
onAttachedToWindow()396     protected void onAttachedToWindow() {
397         super.onAttachedToWindow();
398         updateVisibility();
399     }
400 
getContractedChild()401     public View getContractedChild() {
402         return mContractedChild;
403     }
404 
getExpandedChild()405     public View getExpandedChild() {
406         return mExpandedChild;
407     }
408 
getHeadsUpChild()409     public View getHeadsUpChild() {
410         return mHeadsUpChild;
411     }
412 
413     /**
414      * Sets the contracted view. Child may be null to remove the content view.
415      *
416      * @param child contracted content view to set
417      */
setContractedChild(@ullable View child)418     public void setContractedChild(@Nullable View child) {
419         if (mContractedChild != null) {
420             mOnContentViewInactiveListeners.remove(mContractedChild);
421             mContractedChild.animate().cancel();
422             removeView(mContractedChild);
423         }
424         if (child == null) {
425             mContractedChild = null;
426             mContractedWrapper = null;
427             if (mTransformationStartVisibleType == VISIBLE_TYPE_CONTRACTED) {
428                 mTransformationStartVisibleType = VISIBLE_TYPE_NONE;
429             }
430             return;
431         }
432         addView(child);
433         mContractedChild = child;
434         mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child,
435                 mContainingNotification);
436         // The contracted wrapper has changed. If this is the shown wrapper, we need to update it.
437         updateShownWrapper(mVisibleType);
438     }
439 
getWrapperForView(View child)440     private NotificationViewWrapper getWrapperForView(View child) {
441         if (child == mContractedChild) {
442             return mContractedWrapper;
443         }
444         if (child == mExpandedChild) {
445             return mExpandedWrapper;
446         }
447         if (child == mHeadsUpChild) {
448             return mHeadsUpWrapper;
449         }
450         return null;
451     }
452 
453     /**
454      * Sets the expanded view. Child may be null to remove the content view.
455      *
456      * @param child expanded content view to set
457      */
setExpandedChild(@ullable View child)458     public void setExpandedChild(@Nullable View child) {
459         if (mExpandedChild != null) {
460             mPreviousExpandedRemoteInputIntent = null;
461             if (mExpandedRemoteInput != null) {
462                 mExpandedRemoteInput.onNotificationUpdateOrReset();
463                 if (mExpandedRemoteInput.isActive()) {
464                     if (mExpandedRemoteInputController != null) {
465                         mPreviousExpandedRemoteInputIntent =
466                                 mExpandedRemoteInputController.getPendingIntent();
467                     }
468                     mCachedExpandedRemoteInput = mExpandedRemoteInput;
469                     mCachedExpandedRemoteInputViewController = mExpandedRemoteInputController;
470                     mExpandedRemoteInput.dispatchStartTemporaryDetach();
471                     ((ViewGroup)mExpandedRemoteInput.getParent()).removeView(mExpandedRemoteInput);
472                 }
473             }
474             mOnContentViewInactiveListeners.remove(mExpandedChild);
475             mExpandedChild.animate().cancel();
476             removeView(mExpandedChild);
477             mExpandedRemoteInput = null;
478             if (mExpandedRemoteInputController != null) {
479                 mExpandedRemoteInputController.unbind();
480             }
481             mExpandedRemoteInputController = null;
482         }
483         if (child == null) {
484             mExpandedChild = null;
485             mExpandedWrapper = null;
486             if (mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED) {
487                 mTransformationStartVisibleType = VISIBLE_TYPE_NONE;
488             }
489             if (mVisibleType == VISIBLE_TYPE_EXPANDED) {
490                 selectLayout(false /* animate */, true /* force */);
491             }
492             return;
493         }
494         addView(child);
495         mExpandedChild = child;
496         mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child,
497                 mContainingNotification);
498         if (mContainingNotification != null) {
499             applySystemActions(mExpandedChild, mContainingNotification.getEntry());
500         }
501         // The expanded wrapper has changed. If this is the shown wrapper, we need to update it.
502         updateShownWrapper(mVisibleType);
503     }
504 
505     /**
506      * Sets the heads up view. Child may be null to remove the content view.
507      *
508      * @param child heads up content view to set
509      */
setHeadsUpChild(@ullable View child)510     public void setHeadsUpChild(@Nullable View child) {
511         if (mHeadsUpChild != null) {
512             mPreviousHeadsUpRemoteInputIntent = null;
513             if (mHeadsUpRemoteInput != null) {
514                 mHeadsUpRemoteInput.onNotificationUpdateOrReset();
515                 if (mHeadsUpRemoteInput.isActive()) {
516                     if (mHeadsUpRemoteInputController != null) {
517                         mPreviousHeadsUpRemoteInputIntent =
518                                 mHeadsUpRemoteInputController.getPendingIntent();
519                     }
520                     mCachedHeadsUpRemoteInput = mHeadsUpRemoteInput;
521                     mCachedHeadsUpRemoteInputViewController = mHeadsUpRemoteInputController;
522                     mHeadsUpRemoteInput.dispatchStartTemporaryDetach();
523                     ((ViewGroup)mHeadsUpRemoteInput.getParent()).removeView(mHeadsUpRemoteInput);
524                 }
525             }
526             mOnContentViewInactiveListeners.remove(mHeadsUpChild);
527             mHeadsUpChild.animate().cancel();
528             removeView(mHeadsUpChild);
529             mHeadsUpRemoteInput = null;
530             if (mHeadsUpRemoteInputController != null) {
531                 mHeadsUpRemoteInputController.unbind();
532             }
533             mHeadsUpRemoteInputController = null;
534         }
535         if (child == null) {
536             mHeadsUpChild = null;
537             mHeadsUpWrapper = null;
538             if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) {
539                 mTransformationStartVisibleType = VISIBLE_TYPE_NONE;
540             }
541             if (mVisibleType == VISIBLE_TYPE_HEADSUP) {
542                 selectLayout(false /* animate */, true /* force */);
543             }
544             return;
545         }
546         addView(child);
547         mHeadsUpChild = child;
548         mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child,
549                 mContainingNotification);
550         if (mContainingNotification != null) {
551             applySystemActions(mHeadsUpChild, mContainingNotification.getEntry());
552         }
553         // The heads up wrapper has changed. If this is the shown wrapper, we need to update it.
554         updateShownWrapper(mVisibleType);
555     }
556 
557     /**
558      * Sets the single-line view. Child may be null to remove the view.
559      * @param child single-line content view to set
560      */
setSingleLineView(@ullable HybridNotificationView child)561     public void setSingleLineView(@Nullable HybridNotificationView child) {
562         if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return;
563         if (mSingleLineView != null) {
564             mOnContentViewInactiveListeners.remove(mSingleLineView);
565             mSingleLineView.animate().cancel();
566             removeView(mSingleLineView);
567         }
568         if (child == null) {
569             mSingleLineView = null;
570             if (mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE) {
571                 mTransformationStartVisibleType = VISIBLE_TYPE_NONE;
572             }
573             return;
574         }
575         addView(child);
576         mSingleLineView = child;
577     }
578 
579     @Override
onViewAdded(View child)580     public void onViewAdded(View child) {
581         super.onViewAdded(child);
582         child.setTag(R.id.row_tag_for_content_view, mContainingNotification);
583     }
584 
585     @Override
onVisibilityChanged(View changedView, int visibility)586     protected void onVisibilityChanged(View changedView, int visibility) {
587         super.onVisibilityChanged(changedView, visibility);
588         updateVisibility();
589         if (visibility != VISIBLE && !mOnContentViewInactiveListeners.isEmpty()) {
590             // View is no longer visible so all content views are inactive.
591             // Clone list as runnables may modify the list of listeners
592             ArrayList<Runnable> listeners = new ArrayList<>(
593                     mOnContentViewInactiveListeners.values());
594             for (Runnable r : listeners) {
595                 r.run();
596             }
597             mOnContentViewInactiveListeners.clear();
598         }
599     }
600 
updateVisibility()601     private void updateVisibility() {
602         setVisible(isShown());
603     }
604 
605     @Override
onDetachedFromWindow()606     protected void onDetachedFromWindow() {
607         super.onDetachedFromWindow();
608         getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
609     }
610 
setVisible(final boolean isVisible)611     private void setVisible(final boolean isVisible) {
612         if (isVisible) {
613             // This call can happen multiple times, but removing only removes a single one.
614             // We therefore need to remove the old one.
615             getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
616             // We only animate if we are drawn at least once, otherwise the view might animate when
617             // it's shown the first time
618             getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener);
619         } else {
620             getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
621             mAnimate = false;
622         }
623     }
624 
focusExpandButtonIfNecessary()625     private void focusExpandButtonIfNecessary() {
626         if (mFocusOnVisibilityChange) {
627             NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
628             if (wrapper != null) {
629                 View expandButton = wrapper.getExpandButton();
630                 if (expandButton != null) {
631                     expandButton.requestAccessibilityFocus();
632                 }
633             }
634             mFocusOnVisibilityChange = false;
635         }
636     }
637 
setContentHeight(int contentHeight)638     public void setContentHeight(int contentHeight) {
639         mUnrestrictedContentHeight = Math.max(contentHeight, getMinHeight());
640         int maxContentHeight = mContainingNotification.getIntrinsicHeight()
641                 - getExtraRemoteInputHeight(mExpandedRemoteInput)
642                 - getExtraRemoteInputHeight(mHeadsUpRemoteInput);
643         mContentHeight = Math.min(mUnrestrictedContentHeight, maxContentHeight);
644         selectLayout(mAnimate /* animate */, false /* force */);
645 
646         if (mContractedChild == null) {
647             // Contracted child may be null if this is the public content view and we don't need to
648             // show it.
649             return;
650         }
651 
652         int minHeightHint = getMinContentHeightHint();
653 
654         NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
655         if (wrapper != null) {
656             wrapper.setContentHeight(mUnrestrictedContentHeight, minHeightHint);
657         }
658 
659         wrapper = getVisibleWrapper(mTransformationStartVisibleType);
660         if (wrapper != null) {
661             wrapper.setContentHeight(mUnrestrictedContentHeight, minHeightHint);
662         }
663 
664         updateClipping();
665         invalidateOutline();
666     }
667 
668     /**
669      * @return the minimum apparent height that the wrapper should allow for the purpose
670      *         of aligning elements at the bottom edge. If this is larger than the content
671      *         height, the notification is clipped instead of being further shrunk.
672      */
getMinContentHeightHint()673     private int getMinContentHeightHint() {
674         if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) {
675             return mContext.getResources().getDimensionPixelSize(
676                         com.android.internal.R.dimen.notification_action_list_height);
677         }
678 
679         // Transition between heads-up & expanded, or pinned.
680         if (mHeadsUpChild != null && mExpandedChild != null) {
681             boolean transitioningBetweenHunAndExpanded =
682                     isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) ||
683                     isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP);
684             boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED)
685                     && (mIsHeadsUp || mHeadsUpAnimatingAway)
686                     && mContainingNotification.canShowHeadsUp();
687             if (transitioningBetweenHunAndExpanded || pinned) {
688                 return Math.min(getViewHeight(VISIBLE_TYPE_HEADSUP),
689                         getViewHeight(VISIBLE_TYPE_EXPANDED));
690             }
691         }
692 
693         // Size change of the expanded version
694         if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart != UNDEFINED
695                 && mExpandedChild != null) {
696             return Math.min(mContentHeightAtAnimationStart, getViewHeight(VISIBLE_TYPE_EXPANDED));
697         }
698 
699         int hint;
700         if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) {
701             hint = getViewHeight(VISIBLE_TYPE_HEADSUP);
702             if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isAnimatingAppearance()) {
703                 // While the RemoteInputView is animating its appearance, it should be allowed
704                 // to overlap the hint, therefore no space is reserved for the hint during the
705                 // appearance animation of the RemoteInputView
706                 hint = 0;
707             }
708         } else if (mExpandedChild != null) {
709             hint = getViewHeight(VISIBLE_TYPE_EXPANDED);
710         } else if (mContractedChild != null) {
711             hint = getViewHeight(VISIBLE_TYPE_CONTRACTED)
712                     + mContext.getResources().getDimensionPixelSize(
713                             com.android.internal.R.dimen.notification_action_list_height);
714         } else {
715             hint = getMinHeight();
716         }
717 
718         if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) {
719             hint = Math.min(hint, getViewHeight(VISIBLE_TYPE_EXPANDED));
720         }
721         return hint;
722     }
723 
isTransitioningFromTo(int from, int to)724     private boolean isTransitioningFromTo(int from, int to) {
725         return (mTransformationStartVisibleType == from || mAnimationStartVisibleType == from)
726                 && mVisibleType == to;
727     }
728 
isVisibleOrTransitioning(int type)729     private boolean isVisibleOrTransitioning(int type) {
730         return mVisibleType == type || mTransformationStartVisibleType == type
731                 || mAnimationStartVisibleType == type;
732     }
733 
updateContentTransformation()734     private void updateContentTransformation() {
735         int visibleType = calculateVisibleType();
736         if (getTransformableViewForVisibleType(mVisibleType) == null) {
737             // Case where visible view was removed in middle of transformation. In this case, we
738             // just update immediately to the appropriate view.
739             mVisibleType = visibleType;
740             updateViewVisibilities(visibleType);
741             updateBackgroundColor(false);
742             return;
743         }
744         if (visibleType != mVisibleType) {
745             // A new transformation starts
746             mTransformationStartVisibleType = mVisibleType;
747             final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
748             final TransformableView hiddenView = getTransformableViewForVisibleType(
749                     mTransformationStartVisibleType);
750             shownView.transformFrom(hiddenView, 0.0f);
751             getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
752             hiddenView.transformTo(shownView, 0.0f);
753             mVisibleType = visibleType;
754             updateBackgroundColor(true /* animate */);
755         }
756         if (mForceSelectNextLayout) {
757             forceUpdateVisibilities();
758         }
759         if (mTransformationStartVisibleType != VISIBLE_TYPE_NONE
760                 && mVisibleType != mTransformationStartVisibleType
761                 && getViewForVisibleType(mTransformationStartVisibleType) != null) {
762             final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType);
763             final TransformableView hiddenView = getTransformableViewForVisibleType(
764                     mTransformationStartVisibleType);
765             float transformationAmount = calculateTransformationAmount();
766             shownView.transformFrom(hiddenView, transformationAmount);
767             hiddenView.transformTo(shownView, transformationAmount);
768             updateBackgroundTransformation(transformationAmount);
769         } else {
770             updateViewVisibilities(visibleType);
771             updateBackgroundColor(false);
772         }
773     }
774 
updateBackgroundTransformation(float transformationAmount)775     private void updateBackgroundTransformation(float transformationAmount) {
776         int endColor = getBackgroundColor(mVisibleType);
777         int startColor = getBackgroundColor(mTransformationStartVisibleType);
778         if (endColor != startColor) {
779             if (startColor == 0) {
780                 startColor = mContainingNotification.getBackgroundColorWithoutTint();
781             }
782             if (endColor == 0) {
783                 endColor = mContainingNotification.getBackgroundColorWithoutTint();
784             }
785             endColor = NotificationUtils.interpolateColors(startColor, endColor,
786                     transformationAmount);
787         }
788         mContainingNotification.setContentBackground(endColor, false, this);
789     }
790 
calculateTransformationAmount()791     private float calculateTransformationAmount() {
792         int startHeight = getViewHeight(mTransformationStartVisibleType);
793         int endHeight = getViewHeight(mVisibleType);
794         int progress = Math.abs(mContentHeight - startHeight);
795         int totalDistance = Math.abs(endHeight - startHeight);
796         if (totalDistance == 0) {
797             Log.wtf(TAG, "the total transformation distance is 0"
798                     + "\n StartType: " + mTransformationStartVisibleType + " height: " + startHeight
799                     + "\n VisibleType: " + mVisibleType + " height: " + endHeight
800                     + "\n mContentHeight: " + mContentHeight);
801             return 1.0f;
802         }
803         float amount = (float) progress / (float) totalDistance;
804         return Math.min(1.0f, amount);
805     }
806 
getContentHeight()807     public int getContentHeight() {
808         return mContentHeight;
809     }
810 
getMaxHeight()811     public int getMaxHeight() {
812         if (mExpandedChild != null) {
813             return getViewHeight(VISIBLE_TYPE_EXPANDED)
814                     + getExtraRemoteInputHeight(mExpandedRemoteInput);
815         } else if (mIsHeadsUp && mHeadsUpChild != null && mContainingNotification.canShowHeadsUp()) {
816             return getViewHeight(VISIBLE_TYPE_HEADSUP)
817                     + getExtraRemoteInputHeight(mHeadsUpRemoteInput);
818         } else if (mContractedChild != null) {
819             return getViewHeight(VISIBLE_TYPE_CONTRACTED);
820         }
821         return mNotificationMaxHeight;
822     }
823 
getViewHeight(int visibleType)824     private int getViewHeight(int visibleType) {
825         return getViewHeight(visibleType, false /* forceNoHeader */);
826     }
827 
getViewHeight(int visibleType, boolean forceNoHeader)828     private int getViewHeight(int visibleType, boolean forceNoHeader) {
829         View view = getViewForVisibleType(visibleType);
830         int height = view.getHeight();
831         NotificationViewWrapper viewWrapper = getWrapperForView(view);
832         if (viewWrapper != null) {
833             height += viewWrapper.getHeaderTranslation(forceNoHeader);
834         }
835         return height;
836     }
837 
getMinHeight()838     public int getMinHeight() {
839         return getMinHeight(false /* likeGroupExpanded */);
840     }
841 
getMinHeight(boolean likeGroupExpanded)842     public int getMinHeight(boolean likeGroupExpanded) {
843         if (likeGroupExpanded || !mIsChildInGroup || isGroupExpanded()) {
844             return mContractedChild != null
845                     ? getViewHeight(VISIBLE_TYPE_CONTRACTED) : mMinContractedHeight;
846         } else {
847             if (AsyncHybridViewInflation.isEnabled()) {
848                 if (mSingleLineView != null) {
849                     return getViewHeight(VISIBLE_TYPE_SINGLELINE);
850                 } else {
851                     //TODO(b/217799515): investigate the impact of min-height value
852                     return mMinSingleLineHeight;
853                 }
854             } else {
855                 AsyncHybridViewInflation.assertInLegacyMode();
856                 return mSingleLineView.getHeight();
857             }
858         }
859     }
860 
isGroupExpanded()861     private boolean isGroupExpanded() {
862         return mContainingNotification.isGroupExpanded();
863     }
864 
setClipTopAmount(int clipTopAmount)865     public void setClipTopAmount(int clipTopAmount) {
866         mClipTopAmount = clipTopAmount;
867         updateClipping();
868     }
869 
870 
setClipBottomAmount(int clipBottomAmount)871     public void setClipBottomAmount(int clipBottomAmount) {
872         mClipBottomAmount = clipBottomAmount;
873         updateClipping();
874     }
875 
876     @Override
setTranslationY(float translationY)877     public void setTranslationY(float translationY) {
878         super.setTranslationY(translationY);
879         updateClipping();
880     }
881 
updateClipping()882     private void updateClipping() {
883         if (mClipToActualHeight) {
884             int top = (int) (mClipTopAmount - getTranslationY());
885             int bottom = (int) (mUnrestrictedContentHeight - mClipBottomAmount - getTranslationY());
886             bottom = Math.max(top, bottom);
887             mClipBounds.set(0, top, getWidth(), bottom);
888             setClipBounds(mClipBounds);
889         } else {
890             setClipBounds(null);
891         }
892     }
893 
setClipToActualHeight(boolean clipToActualHeight)894     public void setClipToActualHeight(boolean clipToActualHeight) {
895         mClipToActualHeight = clipToActualHeight;
896         updateClipping();
897     }
898 
selectLayout(boolean animate, boolean force)899     private void selectLayout(boolean animate, boolean force) {
900         if (mContractedChild == null) {
901             return;
902         }
903         if (mUserExpanding) {
904             updateContentTransformation();
905         } else {
906             int visibleType = calculateVisibleType();
907             boolean changedType = visibleType != mVisibleType;
908             if (changedType || force) {
909                 View visibleView = getViewForVisibleType(visibleType);
910                 if (visibleView != null) {
911                     visibleView.setVisibility(VISIBLE);
912                     transferRemoteInputFocus(visibleType);
913                 }
914 
915                 if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null)
916                         || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null)
917                         || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null)
918                         || visibleType == VISIBLE_TYPE_CONTRACTED)) {
919                     animateToVisibleType(visibleType);
920                 } else {
921                     updateViewVisibilities(visibleType);
922                 }
923                 mVisibleType = visibleType;
924                 if (changedType) {
925                     focusExpandButtonIfNecessary();
926                 }
927                 NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType);
928                 if (visibleWrapper != null) {
929                     visibleWrapper.setContentHeight(mUnrestrictedContentHeight,
930                             getMinContentHeightHint());
931                 }
932                 updateBackgroundColor(animate);
933             }
934         }
935     }
936 
forceUpdateVisibilities()937     private void forceUpdateVisibilities() {
938         forceUpdateVisibility(VISIBLE_TYPE_CONTRACTED, mContractedChild, mContractedWrapper);
939         forceUpdateVisibility(VISIBLE_TYPE_EXPANDED, mExpandedChild, mExpandedWrapper);
940         forceUpdateVisibility(VISIBLE_TYPE_HEADSUP, mHeadsUpChild, mHeadsUpWrapper);
941         forceUpdateVisibility(VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView);
942         updateShownWrapper(mVisibleType);
943         fireExpandedVisibleListenerIfVisible();
944         // forceUpdateVisibilities cancels outstanding animations without updating the
945         // mAnimationStartVisibleType. Do so here instead.
946         mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
947         notifySubtreeForAccessibilityContentChange();
948     }
949 
fireExpandedVisibleListenerIfVisible()950     private void fireExpandedVisibleListenerIfVisible() {
951         if (mExpandedVisibleListener != null && mExpandedChild != null && isShown()
952                 && mExpandedChild.getVisibility() == VISIBLE) {
953             Runnable listener = mExpandedVisibleListener;
954             mExpandedVisibleListener = null;
955             listener.run();
956         }
957     }
958 
forceUpdateVisibility(int type, View view, TransformableView wrapper)959     private void forceUpdateVisibility(int type, View view, TransformableView wrapper) {
960         if (view == null) {
961             return;
962         }
963         boolean visible = mVisibleType == type
964                 || mTransformationStartVisibleType == type;
965         if (!visible) {
966             view.setVisibility(INVISIBLE);
967         } else {
968             wrapper.setVisible(true);
969         }
970     }
971 
updateBackgroundColor(boolean animate)972     public void updateBackgroundColor(boolean animate) {
973         int customBackgroundColor = getBackgroundColor(mVisibleType);
974         mContainingNotification.setContentBackground(customBackgroundColor, animate, this);
975     }
976 
setBackgroundTintColor(int color)977     public void setBackgroundTintColor(int color) {
978         boolean colorized = mNotificationEntry.getSbn().getNotification().isColorized();
979         if (mExpandedSmartReplyView != null) {
980             mExpandedSmartReplyView.setBackgroundTintColor(color, colorized);
981         }
982         if (mHeadsUpSmartReplyView != null) {
983             mHeadsUpSmartReplyView.setBackgroundTintColor(color, colorized);
984         }
985         if (mExpandedRemoteInput != null) {
986             mExpandedRemoteInput.setBackgroundTintColor(color, colorized);
987         }
988         if (mHeadsUpRemoteInput != null) {
989             mHeadsUpRemoteInput.setBackgroundTintColor(color, colorized);
990         }
991     }
992 
getVisibleType()993     public int getVisibleType() {
994         return mVisibleType;
995     }
996 
getBackgroundColorForExpansionState()997     public int getBackgroundColorForExpansionState() {
998         // When expanding or user locked we want the new type, when collapsing we want
999         // the original type
1000         final int visibleType = (
1001                 isGroupExpanded() || mContainingNotification.isUserLocked())
1002                     ? calculateVisibleType()
1003                     : getVisibleType();
1004         return getBackgroundColor(visibleType);
1005     }
1006 
getBackgroundColor(int visibleType)1007     public int getBackgroundColor(int visibleType) {
1008         NotificationViewWrapper currentVisibleWrapper = getVisibleWrapper(visibleType);
1009         int customBackgroundColor = 0;
1010         if (currentVisibleWrapper != null) {
1011             customBackgroundColor = currentVisibleWrapper.getCustomBackgroundColor();
1012         }
1013         return customBackgroundColor;
1014     }
1015 
updateViewVisibilities(int visibleType)1016     private void updateViewVisibilities(int visibleType) {
1017         updateViewVisibility(visibleType, VISIBLE_TYPE_CONTRACTED,
1018                 mContractedChild, mContractedWrapper);
1019         updateViewVisibility(visibleType, VISIBLE_TYPE_EXPANDED,
1020                 mExpandedChild, mExpandedWrapper);
1021         updateViewVisibility(visibleType, VISIBLE_TYPE_HEADSUP,
1022                 mHeadsUpChild, mHeadsUpWrapper);
1023         updateViewVisibility(visibleType, VISIBLE_TYPE_SINGLELINE,
1024                 mSingleLineView, mSingleLineView);
1025         updateShownWrapper(visibleType);
1026         fireExpandedVisibleListenerIfVisible();
1027         // updateViewVisibilities cancels outstanding animations without updating the
1028         // mAnimationStartVisibleType. Do so here instead.
1029         mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
1030         notifySubtreeForAccessibilityContentChange();
1031     }
1032 
updateViewVisibility(int visibleType, int type, View view, TransformableView wrapper)1033     private void updateViewVisibility(int visibleType, int type, View view,
1034             TransformableView wrapper) {
1035         if (view != null) {
1036             wrapper.setVisible(visibleType == type);
1037         }
1038     }
1039 
1040     /**
1041      * Called when the currently shown wrapper is potentially affected by a change to the
1042      * {mVisibleType} or the user-visibility of this view.
1043      *
1044      * @see View#isShown()
1045      */
updateShownWrapper(int visibleType)1046     private void updateShownWrapper(int visibleType) {
1047         final NotificationViewWrapper shownWrapper = isShown() ? getVisibleWrapper(visibleType)
1048                 : null;
1049 
1050         if (mShownWrapper != shownWrapper) {
1051             NotificationViewWrapper hiddenWrapper = mShownWrapper;
1052             mShownWrapper = shownWrapper;
1053             if (hiddenWrapper != null) {
1054                 hiddenWrapper.onContentShown(false);
1055             }
1056             if (shownWrapper != null) {
1057                 shownWrapper.onContentShown(true);
1058             }
1059         }
1060     }
1061 
animateToVisibleType(int visibleType)1062     private void animateToVisibleType(int visibleType) {
1063         final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
1064         final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType);
1065         if (shownView == hiddenView || hiddenView == null) {
1066             shownView.setVisible(true);
1067             return;
1068         }
1069         mAnimationStartVisibleType = mVisibleType;
1070         shownView.transformFrom(hiddenView);
1071         getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
1072         updateShownWrapper(visibleType);
1073         hiddenView.transformTo(shownView, new Runnable() {
1074             @Override
1075             public void run() {
1076                 if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) {
1077                     hiddenView.setVisible(false);
1078                 }
1079                 mAnimationStartVisibleType = VISIBLE_TYPE_NONE;
1080                 notifySubtreeForAccessibilityContentChange();
1081             }
1082         });
1083         fireExpandedVisibleListenerIfVisible();
1084     }
1085 
transferRemoteInputFocus(int visibleType)1086     private void transferRemoteInputFocus(int visibleType) {
1087         if (visibleType == VISIBLE_TYPE_HEADSUP
1088                 && mHeadsUpRemoteInputController != null
1089                 && mExpandedRemoteInputController != null
1090                 && mExpandedRemoteInputController.isActive()) {
1091             mHeadsUpRemoteInputController.stealFocusFrom(mExpandedRemoteInputController);
1092         }
1093         if (visibleType == VISIBLE_TYPE_EXPANDED
1094                 && mExpandedRemoteInputController != null
1095                 && mHeadsUpRemoteInputController != null
1096                 && mHeadsUpRemoteInputController.isActive()) {
1097             mExpandedRemoteInputController.stealFocusFrom(mHeadsUpRemoteInputController);
1098         }
1099     }
1100 
1101     @Override
notifySubtreeAccessibilityStateChanged(View child, View source, int changeType)1102     public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
1103         if (isAnimatingVisibleType()) {
1104             // Don't send A11y events while animating to reduce Jank.
1105             return;
1106         }
1107         super.notifySubtreeAccessibilityStateChanged(child, source, changeType);
1108     }
1109 
notifySubtreeForAccessibilityContentChange()1110     private void notifySubtreeForAccessibilityContentChange() {
1111         if (mParent != null) {
1112             mParent.notifySubtreeAccessibilityStateChanged(this, this,
1113                     AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
1114         }
1115     }
1116 
1117     /**
1118      * @param visibleType one of the static enum types in this view
1119      * @return the corresponding transformable view according to the given visible type
1120      */
getTransformableViewForVisibleType(int visibleType)1121     private TransformableView getTransformableViewForVisibleType(int visibleType) {
1122         switch (visibleType) {
1123             case VISIBLE_TYPE_EXPANDED:
1124                 return mExpandedWrapper;
1125             case VISIBLE_TYPE_HEADSUP:
1126                 return mHeadsUpWrapper;
1127             case VISIBLE_TYPE_SINGLELINE:
1128                 return mSingleLineView;
1129             default:
1130                 return mContractedWrapper;
1131         }
1132     }
1133 
1134     /**
1135      * @param visibleType one of the static enum types in this view
1136      * @return the corresponding view according to the given visible type
1137      */
getViewForVisibleType(int visibleType)1138     private View getViewForVisibleType(int visibleType) {
1139         switch (visibleType) {
1140             case VISIBLE_TYPE_EXPANDED:
1141                 return mExpandedChild;
1142             case VISIBLE_TYPE_HEADSUP:
1143                 return mHeadsUpChild;
1144             case VISIBLE_TYPE_SINGLELINE:
1145                 return mSingleLineView;
1146             default:
1147                 return mContractedChild;
1148         }
1149     }
1150 
getAllViews()1151     public @NonNull View[] getAllViews() {
1152         return new View[] {
1153                 mContractedChild,
1154                 mHeadsUpChild,
1155                 mExpandedChild,
1156                 mSingleLineView };
1157     }
1158 
getVisibleWrapper()1159     public NotificationViewWrapper getVisibleWrapper() {
1160         return getVisibleWrapper(mVisibleType);
1161     }
1162 
getVisibleWrapper(int visibleType)1163     public NotificationViewWrapper getVisibleWrapper(int visibleType) {
1164         switch (visibleType) {
1165             case VISIBLE_TYPE_EXPANDED:
1166                 return mExpandedWrapper;
1167             case VISIBLE_TYPE_HEADSUP:
1168                 return mHeadsUpWrapper;
1169             case VISIBLE_TYPE_CONTRACTED:
1170                 return mContractedWrapper;
1171             default:
1172                 return null;
1173         }
1174     }
1175 
1176     /**
1177      * @return one of the static enum types in this view, calculated from the current state
1178      */
calculateVisibleType()1179     public int calculateVisibleType() {
1180         if (mUserExpanding) {
1181             int height = !mIsChildInGroup || isGroupExpanded()
1182                     || mContainingNotification.isExpanded(true /* allowOnKeyguard */)
1183                     ? mContainingNotification.getMaxContentHeight()
1184                     : mContainingNotification.getShowingLayout().getMinHeight();
1185             if (height == 0) {
1186                 height = mContentHeight;
1187             }
1188             int expandedVisualType = getVisualTypeForHeight(height);
1189             int collapsedVisualType = mIsChildInGroup && !isGroupExpanded()
1190                     ? VISIBLE_TYPE_SINGLELINE
1191                     : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight());
1192             return mTransformationStartVisibleType == collapsedVisualType
1193                     ? expandedVisualType
1194                     : collapsedVisualType;
1195         }
1196         int intrinsicHeight = mContainingNotification.getIntrinsicHeight();
1197         int viewHeight = mContentHeight;
1198         if (intrinsicHeight != 0) {
1199             // the intrinsicHeight might be 0 because it was just reset.
1200             viewHeight = Math.min(mContentHeight, intrinsicHeight);
1201         }
1202         return getVisualTypeForHeight(viewHeight);
1203     }
1204 
getVisualTypeForHeight(float viewHeight)1205     private int getVisualTypeForHeight(float viewHeight) {
1206         boolean noExpandedChild = mExpandedChild == null;
1207         if (!noExpandedChild && viewHeight == getViewHeight(VISIBLE_TYPE_EXPANDED)) {
1208             return VISIBLE_TYPE_EXPANDED;
1209         }
1210         if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) {
1211             return VISIBLE_TYPE_SINGLELINE;
1212         }
1213 
1214         if ((mIsHeadsUp || mHeadsUpAnimatingAway) && mHeadsUpChild != null
1215                 && mContainingNotification.canShowHeadsUp()) {
1216             if (viewHeight <= getViewHeight(VISIBLE_TYPE_HEADSUP) || noExpandedChild) {
1217                 return VISIBLE_TYPE_HEADSUP;
1218             } else {
1219                 return VISIBLE_TYPE_EXPANDED;
1220             }
1221         } else {
1222             if (noExpandedChild || (mContractedChild != null
1223                     && viewHeight <= getViewHeight(VISIBLE_TYPE_CONTRACTED)
1224                     && (!mIsChildInGroup || isGroupExpanded()
1225                             || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
1226                 return VISIBLE_TYPE_CONTRACTED;
1227             } else if (!noExpandedChild) {
1228                 return VISIBLE_TYPE_EXPANDED;
1229             } else {
1230                 return VISIBLE_TYPE_NONE;
1231             }
1232         }
1233     }
1234 
isContentExpandable()1235     public boolean isContentExpandable() {
1236         return mIsContentExpandable;
1237     }
1238 
setHeadsUp(boolean headsUp)1239     public void setHeadsUp(boolean headsUp) {
1240         mIsHeadsUp = headsUp;
1241         selectLayout(false /* animate */, true /* force */);
1242         updateExpandButtons(mExpandable);
1243     }
1244 
1245     @Override
hasOverlappingRendering()1246     public boolean hasOverlappingRendering() {
1247 
1248         // This is not really true, but good enough when fading from the contracted to the expanded
1249         // layout, and saves us some layers.
1250         return false;
1251     }
1252 
setLegacy(boolean legacy)1253     public void setLegacy(boolean legacy) {
1254         mLegacy = legacy;
1255         updateLegacy();
1256     }
1257 
updateLegacy()1258     private void updateLegacy() {
1259         if (mContractedChild != null) {
1260             mContractedWrapper.setLegacy(mLegacy);
1261         }
1262         if (mExpandedChild != null) {
1263             mExpandedWrapper.setLegacy(mLegacy);
1264         }
1265         if (mHeadsUpChild != null) {
1266             mHeadsUpWrapper.setLegacy(mLegacy);
1267         }
1268     }
1269 
setIsChildInGroup(boolean isChildInGroup)1270     public void setIsChildInGroup(boolean isChildInGroup) {
1271         mIsChildInGroup = isChildInGroup;
1272         if (mContractedChild != null) {
1273             mContractedWrapper.setIsChildInGroup(mIsChildInGroup);
1274         }
1275         if (mExpandedChild != null) {
1276             mExpandedWrapper.setIsChildInGroup(mIsChildInGroup);
1277         }
1278         if (mHeadsUpChild != null) {
1279             mHeadsUpWrapper.setIsChildInGroup(mIsChildInGroup);
1280         }
1281         updateAllSingleLineViews();
1282     }
1283 
onNotificationUpdated(NotificationEntry entry)1284     public void onNotificationUpdated(NotificationEntry entry) {
1285         mNotificationEntry = entry;
1286         mBeforeN = entry.targetSdk < Build.VERSION_CODES.N;
1287         updateAllSingleLineViews();
1288         ExpandableNotificationRow row = entry.getRow();
1289         if (mContractedChild != null) {
1290             mContractedWrapper.onContentUpdated(row);
1291         }
1292         if (mExpandedChild != null) {
1293             mExpandedWrapper.onContentUpdated(row);
1294         }
1295         if (mHeadsUpChild != null) {
1296             mHeadsUpWrapper.onContentUpdated(row);
1297         }
1298         applyRemoteInputAndSmartReply();
1299         updateLegacy();
1300         mForceSelectNextLayout = true;
1301         mPreviousExpandedRemoteInputIntent = null;
1302         mPreviousHeadsUpRemoteInputIntent = null;
1303         applySystemActions(mExpandedChild, entry);
1304         applySystemActions(mHeadsUpChild, entry);
1305     }
1306 
1307     private void updateAllSingleLineViews() {
1308         updateSingleLineView();
1309     }
1310 
1311     private void updateSingleLineView() {
1312         try {
1313             Trace.beginSection("NotifContentView#updateSingleLineView");
1314             if (AsyncHybridViewInflation.isEnabled()) {
1315                 return;
1316             }
1317             AsyncHybridViewInflation.assertInLegacyMode();
1318             if (mIsChildInGroup) {
1319                 boolean isNewView = mSingleLineView == null;
1320                 mSingleLineView = mHybridGroupManager.bindFromNotification(
1321                         /* reusableView = */ mSingleLineView,
1322                         /* contentView = */ mContractedChild,
1323                         /* notification = */ mNotificationEntry.getSbn(),
1324                         /* parent = */ this
1325                 );
1326                 if (isNewView && mSingleLineView != null) {
1327                     updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE,
1328                             mSingleLineView, mSingleLineView);
1329                 }
1330             } else if (mSingleLineView != null) {
1331                 removeView(mSingleLineView);
1332                 mSingleLineView = null;
1333             }
1334         } finally {
1335             Trace.endSection();
1336         }
1337     }
1338 
1339     /**
1340      * Returns whether the {@link Notification} represented by entry has a free-form remote input.
1341      * Such an input can be used e.g. to implement smart reply buttons - by passing the replies
1342      * through the remote input.
1343      */
1344     public static boolean hasFreeformRemoteInput(NotificationEntry entry) {
1345         Notification notification = entry.getSbn().getNotification();
1346         return null != notification.findRemoteInputActionPair(true /* freeform */);
1347     }
1348 
1349     private void applyRemoteInputAndSmartReply() {
1350         if (mRemoteInputController != null) {
1351             applyRemoteInput();
1352         }
1353 
1354         if (mCurrentSmartReplyState == null) {
1355             if (DEBUG) {
1356                 Log.d(TAG, "InflatedSmartReplies are null, don't add smart replies.");
1357             }
1358             return;
1359         }
1360         if (DEBUG) {
1361             Log.d(TAG, String.format("Adding suggestions for %s, %d actions, and %d replies.",
1362                     mNotificationEntry.getSbn().getKey(),
1363                     mCurrentSmartReplyState.getSmartActionsList().size(),
1364                     mCurrentSmartReplyState.getSmartRepliesList().size()));
1365         }
1366         applySmartReplyView();
1367     }
1368 
1369     private void applyRemoteInput() {
1370         boolean hasFreeformRemoteInput = hasFreeformRemoteInput(mNotificationEntry);
1371         if (mExpandedChild != null) {
1372             RemoteInputViewData expandedData = applyRemoteInput(mExpandedChild, mNotificationEntry,
1373                     hasFreeformRemoteInput, mPreviousExpandedRemoteInputIntent,
1374                     mCachedExpandedRemoteInput, mCachedExpandedRemoteInputViewController,
1375                     mExpandedWrapper);
1376             mExpandedRemoteInput = expandedData.mView;
1377             mExpandedRemoteInputController = expandedData.mController;
1378             if (mExpandedRemoteInputController != null) {
1379                 mExpandedRemoteInputController.bind();
1380             }
1381         } else {
1382             mExpandedRemoteInput = null;
1383             if (mExpandedRemoteInputController != null) {
1384                 mExpandedRemoteInputController.unbind();
1385             }
1386             mExpandedRemoteInputController = null;
1387         }
1388         if (mCachedExpandedRemoteInput != null
1389                 && mCachedExpandedRemoteInput != mExpandedRemoteInput) {
1390             // We had a cached remote input but didn't reuse it. Clean up required.
1391             mCachedExpandedRemoteInput.dispatchFinishTemporaryDetach();
1392         }
1393         mCachedExpandedRemoteInput = null;
1394         mCachedExpandedRemoteInputViewController = null;
1395 
1396         if (mHeadsUpChild != null) {
1397             RemoteInputViewData headsUpData = applyRemoteInput(mHeadsUpChild, mNotificationEntry,
1398                     hasFreeformRemoteInput, mPreviousHeadsUpRemoteInputIntent,
1399                     mCachedHeadsUpRemoteInput, mCachedHeadsUpRemoteInputViewController,
1400                     mHeadsUpWrapper);
1401             mHeadsUpRemoteInput = headsUpData.mView;
1402             mHeadsUpRemoteInputController = headsUpData.mController;
1403             if (mHeadsUpRemoteInputController != null) {
1404                 mHeadsUpRemoteInputController.bind();
1405             }
1406         } else {
1407             mHeadsUpRemoteInput = null;
1408             if (mHeadsUpRemoteInputController != null) {
1409                 mHeadsUpRemoteInputController.unbind();
1410             }
1411             mHeadsUpRemoteInputController = null;
1412         }
1413         if (mCachedHeadsUpRemoteInput != null
1414                 && mCachedHeadsUpRemoteInput != mHeadsUpRemoteInput) {
1415             // We had a cached remote input but didn't reuse it. Clean up required.
1416             mCachedHeadsUpRemoteInput.dispatchFinishTemporaryDetach();
1417         }
1418         mCachedHeadsUpRemoteInput = null;
1419         mCachedHeadsUpRemoteInputViewController = null;
1420     }
1421 
1422     private RemoteInputViewData applyRemoteInput(View view, NotificationEntry entry,
1423             boolean hasRemoteInput, PendingIntent existingPendingIntent, RemoteInputView cachedView,
1424             RemoteInputViewController cachedController, NotificationViewWrapper wrapper) {
1425         RemoteInputViewData result = new RemoteInputViewData();
1426         View actionContainerCandidate = view.findViewById(
1427                 com.android.internal.R.id.actions_container);
1428         if (actionContainerCandidate instanceof FrameLayout) {
1429             result.mView = view.findViewWithTag(RemoteInputView.VIEW_TAG);
1430 
1431             if (result.mView != null) {
1432                 result.mView.onNotificationUpdateOrReset();
1433                 result.mController = result.mView.getController();
1434             }
1435 
1436             if (result.mView == null && hasRemoteInput) {
1437                 ViewGroup actionContainer = (FrameLayout) actionContainerCandidate;
1438                 if (cachedView == null) {
1439                     RemoteInputView riv = RemoteInputView.inflate(
1440                             mContext, actionContainer, entry, mRemoteInputController);
1441 
1442                     riv.setVisibility(View.GONE);
1443                     actionContainer.addView(riv, new LayoutParams(
1444                             ViewGroup.LayoutParams.MATCH_PARENT,
1445                             ViewGroup.LayoutParams.MATCH_PARENT)
1446                     );
1447                     result.mView = riv;
1448                     // Create a new controller for the view. The lifetime of the controller is 1:1
1449                     // with that of the view.
1450                     RemoteInputViewSubcomponent subcomponent = mRemoteInputSubcomponentFactory
1451                             .create(result.mView, mRemoteInputController);
1452                     result.mController = subcomponent.getController();
1453                     result.mView.setController(result.mController);
1454                 } else {
1455                     actionContainer.addView(cachedView);
1456                     cachedView.dispatchFinishTemporaryDetach();
1457                     cachedView.requestFocus();
1458                     result.mView = cachedView;
1459                     result.mController = cachedController;
1460                 }
1461             }
1462             if (hasRemoteInput) {
1463                 result.mView.setWrapper(wrapper);
1464                 result.mView.addOnVisibilityChangedListener(this::setRemoteInputVisible);
1465 
1466                 if (existingPendingIntent != null || result.mView.isActive()) {
1467                     // The current action could be gone, or the pending intent no longer valid.
1468                     // If we find a matching action in the new notification, focus, otherwise close.
1469                     Notification.Action[] actions = entry.getSbn().getNotification().actions;
1470                     if (existingPendingIntent != null) {
1471                         result.mController.setPendingIntent(existingPendingIntent);
1472                     }
1473                     if (result.mController.updatePendingIntentFromActions(actions)) {
1474                         if (!result.mController.isActive()) {
1475                             result.mController.focus();
1476                         }
1477                     } else {
1478                         if (result.mController.isActive()) {
1479                             result.mController.close();
1480                         }
1481                     }
1482                 }
1483             }
1484             if (result.mView != null) {
1485                 int backgroundColor = entry.getRow().getCurrentBackgroundTint();
1486                 boolean colorized = entry.getSbn().getNotification().isColorized();
1487                 result.mView.setBackgroundTintColor(backgroundColor, colorized);
1488             }
1489         }
1490         return result;
1491     }
1492 
1493     /**
1494      * Call to update state of the bubble button (i.e. does it show bubble or unbubble or no
1495      * icon at all).
1496      *
1497      * @param entry the new entry to use.
1498      */
1499     public void updateBubbleButton(NotificationEntry entry) {
1500         applyBubbleAction(mExpandedChild, entry);
1501     }
1502 
1503     /**
1504      * Setup icon buttons provided by System UI.
1505      */
1506     private void applySystemActions(View layout, NotificationEntry entry) {
1507         applySnoozeAction(layout);
1508         applyBubbleAction(layout, entry);
1509     }
1510 
1511     private void applyBubbleAction(View layout, NotificationEntry entry) {
1512         if (layout == null || mContainingNotification == null || mPeopleIdentifier == null) {
1513             return;
1514         }
1515         ImageView bubbleButton = layout.findViewById(com.android.internal.R.id.bubble_button);
1516         View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container);
1517         ViewGroup actionListMarginTarget = layout.findViewById(
1518                 com.android.internal.R.id.notification_action_list_margin_target);
1519         if (bubbleButton == null || actionContainer == null) {
1520             return;
1521         }
1522 
1523         if (shouldShowBubbleButton(entry)) {
1524             // explicitly resolve drawable resource using SystemUI's theme
1525             Drawable d = mContext.getDrawable(entry.isBubble()
1526                     ? com.android.wm.shell.R.drawable.bubble_ic_stop_bubble
1527                     : com.android.wm.shell.R.drawable.bubble_ic_create_bubble);
1528 
1529             String contentDescription = mContext.getResources().getString(entry.isBubble()
1530                     ? R.string.notification_conversation_unbubble
1531                     : R.string.notification_conversation_bubble);
1532 
1533             bubbleButton.setContentDescription(contentDescription);
1534             bubbleButton.setImageDrawable(d);
1535             bubbleButton.setOnClickListener(mContainingNotification.getBubbleClickListener());
1536             bubbleButton.setVisibility(VISIBLE);
1537             actionContainer.setVisibility(VISIBLE);
1538             // Set notification_action_list_margin_target's bottom margin to 0 when showing bubble
1539             if (actionListMarginTarget != null) {
1540                 ViewGroup.LayoutParams lp = actionListMarginTarget.getLayoutParams();
1541                 if (lp instanceof ViewGroup.MarginLayoutParams) {
1542                     final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
1543                     if (mlp.bottomMargin > 0) {
1544                         mlp.setMargins(mlp.leftMargin, mlp.topMargin, mlp.rightMargin, 0);
1545                     }
1546                 }
1547             }
1548         } else  {
1549             bubbleButton.setVisibility(GONE);
1550         }
1551     }
1552 
1553     @MainThread
1554     public void setBubblesEnabledForUser(boolean enabled) {
1555         mBubblesEnabledForUser = enabled;
1556 
1557         applyBubbleAction(mExpandedChild, mNotificationEntry);
1558         applyBubbleAction(mHeadsUpChild, mNotificationEntry);
1559     }
1560 
1561     @VisibleForTesting
1562     boolean shouldShowBubbleButton(NotificationEntry entry) {
1563         boolean isPersonWithShortcut =
1564                 mPeopleIdentifier.getPeopleNotificationType(entry)
1565                         >= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
1566         return mBubblesEnabledForUser
1567                 && isPersonWithShortcut
1568                 && entry.getBubbleMetadata() != null;
1569     }
1570 
1571     private void applySnoozeAction(View layout) {
1572         if (layout == null || mContainingNotification == null) {
1573             return;
1574         }
1575         ImageView snoozeButton = layout.findViewById(com.android.internal.R.id.snooze_button);
1576         View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container);
1577         if (snoozeButton == null || actionContainer == null) {
1578             return;
1579         }
1580         // Notification.Builder can 'disable' the snooze button to prevent it from being shown here
1581         boolean snoozeDisabled = !snoozeButton.isEnabled();
1582         if (!mContainingNotification.getShowSnooze() || snoozeDisabled) {
1583             snoozeButton.setVisibility(GONE);
1584             return;
1585         }
1586 
1587         // explicitly resolve drawable resource using SystemUI's theme
1588         Drawable snoozeDrawable = mContext.getDrawable(R.drawable.ic_snooze);
1589         snoozeButton.setImageDrawable(snoozeDrawable);
1590 
1591         final NotificationSnooze snoozeGuts = (NotificationSnooze) LayoutInflater.from(mContext)
1592                 .inflate(R.layout.notification_snooze, null, false);
1593         final String snoozeDescription = mContext.getString(
1594                 R.string.notification_menu_snooze_description);
1595         final NotificationMenuRowPlugin.MenuItem snoozeMenuItem =
1596                 new NotificationMenuRow.NotificationMenuItem(
1597                         mContext, snoozeDescription, snoozeGuts, R.drawable.ic_snooze);
1598         snoozeButton.setContentDescription(
1599                 mContext.getResources().getString(R.string.notification_menu_snooze_description));
1600         snoozeButton.setOnClickListener(
1601                 mContainingNotification.getSnoozeClickListener(snoozeMenuItem));
1602         snoozeButton.setVisibility(VISIBLE);
1603         actionContainer.setVisibility(VISIBLE);
1604     }
1605 
1606     private void applySmartReplyView() {
1607         if (mContractedChild != null) {
1608             applyExternalSmartReplyState(mContractedChild, mCurrentSmartReplyState);
1609         }
1610         if (mExpandedChild != null) {
1611             applyExternalSmartReplyState(mExpandedChild, mCurrentSmartReplyState);
1612             mExpandedSmartReplyView = applySmartReplyView(mExpandedChild, mCurrentSmartReplyState,
1613                     mNotificationEntry, mExpandedInflatedSmartReplies);
1614             if (mExpandedSmartReplyView != null) {
1615                 SmartReplyView.SmartReplies smartReplies =
1616                         mCurrentSmartReplyState.getSmartReplies();
1617                 SmartReplyView.SmartActions smartActions =
1618                         mCurrentSmartReplyState.getSmartActions();
1619                 if (smartReplies != null || smartActions != null) {
1620                     int numSmartReplies = smartReplies == null ? 0 : smartReplies.choices.size();
1621                     int numSmartActions = smartActions == null ? 0 : smartActions.actions.size();
1622                     boolean fromAssistant = smartReplies == null
1623                             ? smartActions.fromAssistant
1624                             : smartReplies.fromAssistant;
1625                     boolean editBeforeSending = smartReplies != null
1626                             && mSmartReplyConstants.getEffectiveEditChoicesBeforeSending(
1627                                     smartReplies.remoteInput.getEditChoicesBeforeSending());
1628 
1629                     mSmartReplyController.smartSuggestionsAdded(mNotificationEntry, numSmartReplies,
1630                             numSmartActions, fromAssistant, editBeforeSending);
1631                 }
1632             }
1633         }
1634         if (mHeadsUpChild != null) {
1635             applyExternalSmartReplyState(mHeadsUpChild, mCurrentSmartReplyState);
1636             if (mSmartReplyConstants.getShowInHeadsUp()) {
1637                 mHeadsUpSmartReplyView = applySmartReplyView(mHeadsUpChild, mCurrentSmartReplyState,
1638                         mNotificationEntry, mHeadsUpInflatedSmartReplies);
1639             }
1640         }
1641     }
1642 
1643     private void applyExternalSmartReplyState(View view, InflatedSmartReplyState state) {
1644         boolean hasPhishingAlert = state != null && state.getHasPhishingAction();
1645         View phishingAlertIcon = view.findViewById(com.android.internal.R.id.phishing_alert);
1646         if (phishingAlertIcon != null) {
1647             if (DEBUG) {
1648                 Log.d(TAG, "Setting 'phishing_alert' view visible=" + hasPhishingAlert + ".");
1649             }
1650             phishingAlertIcon.setVisibility(hasPhishingAlert ? View.VISIBLE : View.GONE);
1651         }
1652         List<Integer> suppressedActionIndices = state != null
1653                 ? state.getSuppressedActionIndices()
1654                 : Collections.emptyList();
1655         ViewGroup actionsList = view.findViewById(com.android.internal.R.id.actions);
1656         if (actionsList != null) {
1657             if (DEBUG && !suppressedActionIndices.isEmpty()) {
1658                 Log.d(TAG, "Suppressing actions with indices: " + suppressedActionIndices);
1659             }
1660             for (int i = 0; i < actionsList.getChildCount(); i++) {
1661                 View actionBtn = actionsList.getChildAt(i);
1662                 Object actionIndex =
1663                         actionBtn.getTag(com.android.internal.R.id.notification_action_index_tag);
1664                 boolean suppressAction = actionIndex instanceof Integer
1665                         && suppressedActionIndices.contains(actionIndex);
1666                 actionBtn.setVisibility(suppressAction ? View.GONE : View.VISIBLE);
1667             }
1668         }
1669     }
1670 
1671     @Nullable
1672     private static SmartReplyView applySmartReplyView(View view,
1673             InflatedSmartReplyState smartReplyState,
1674             NotificationEntry entry, InflatedSmartReplyViewHolder inflatedSmartReplyViewHolder) {
1675         View smartReplyContainerCandidate = view.findViewById(
1676                 com.android.internal.R.id.smart_reply_container);
1677         if (!(smartReplyContainerCandidate instanceof LinearLayout)) {
1678             return null;
1679         }
1680 
1681         LinearLayout smartReplyContainer = (LinearLayout) smartReplyContainerCandidate;
1682         if (!SmartReplyStateInflaterKt.shouldShowSmartReplyView(entry, smartReplyState)) {
1683             smartReplyContainer.setVisibility(View.GONE);
1684             return null;
1685         }
1686 
1687         // Search for an existing SmartReplyView
1688         int index = 0;
1689         final int childCount = smartReplyContainer.getChildCount();
1690         for (; index < childCount; index++) {
1691             View child = smartReplyContainer.getChildAt(index);
1692             if (child.getId() == R.id.smart_reply_view && child instanceof SmartReplyView) {
1693                 break;
1694             }
1695         }
1696 
1697         if (index < childCount) {
1698             // If we already have a SmartReplyView - replace it with the newly inflated one. The
1699             // newly inflated one is connected to the new inflated smart reply/action buttons.
1700             smartReplyContainer.removeViewAt(index);
1701         }
1702         SmartReplyView smartReplyView = null;
1703         if (inflatedSmartReplyViewHolder != null
1704                 && inflatedSmartReplyViewHolder.getSmartReplyView() != null) {
1705             smartReplyView = inflatedSmartReplyViewHolder.getSmartReplyView();
1706             smartReplyContainer.addView(smartReplyView, index);
1707         }
1708         if (smartReplyView != null) {
1709             smartReplyView.resetSmartSuggestions(smartReplyContainer);
1710             smartReplyView.addPreInflatedButtons(
1711                     inflatedSmartReplyViewHolder.getSmartSuggestionButtons());
1712             // Ensure the colors of the smart suggestion buttons are up-to-date.
1713             int backgroundColor = entry.getRow().getCurrentBackgroundTint();
1714             boolean colorized = entry.getSbn().getNotification().isColorized();
1715             smartReplyView.setBackgroundTintColor(backgroundColor, colorized);
1716             smartReplyContainer.setVisibility(View.VISIBLE);
1717         }
1718         return smartReplyView;
1719     }
1720 
1721     /**
1722      * Set pre-inflated views necessary to display smart replies and actions in the expanded
1723      * notification state.
1724      *
1725      * @param inflatedSmartReplies the pre-inflated state to add to this view. If null the existing
1726      * {@link SmartReplyView} related to the expanded notification state is cleared.
1727      */
1728     public void setExpandedInflatedSmartReplies(
1729             @Nullable InflatedSmartReplyViewHolder inflatedSmartReplies) {
1730         mExpandedInflatedSmartReplies = inflatedSmartReplies;
1731         if (inflatedSmartReplies == null) {
1732             mExpandedSmartReplyView = null;
1733         }
1734     }
1735 
1736     /**
1737      * Set pre-inflated views necessary to display smart replies and actions in the heads-up
1738      * notification state.
1739      *
1740      * @param inflatedSmartReplies the pre-inflated state to add to this view. If null the existing
1741      * {@link SmartReplyView} related to the heads-up notification state is cleared.
1742      */
1743     public void setHeadsUpInflatedSmartReplies(
1744             @Nullable InflatedSmartReplyViewHolder inflatedSmartReplies) {
1745         mHeadsUpInflatedSmartReplies = inflatedSmartReplies;
1746         if (inflatedSmartReplies == null) {
1747             mHeadsUpSmartReplyView = null;
1748         }
1749     }
1750 
1751     /**
1752      * Set pre-inflated replies and actions for the notification.
1753      * This can be relevant to any state of the notification, even contracted, because smart actions
1754      * may cause a phishing alert to be made visible.
1755      * @param smartReplyState the pre-inflated list of replies and actions
1756      */
1757     public void setInflatedSmartReplyState(
1758             @NonNull InflatedSmartReplyState smartReplyState) {
1759         mCurrentSmartReplyState = smartReplyState;
1760     }
1761 
1762     /**
1763      * Returns the smart replies and actions currently shown in the notification.
1764      */
1765     @Nullable public InflatedSmartReplyState getCurrentSmartReplyState() {
1766         return mCurrentSmartReplyState;
1767     }
1768 
1769     public void closeRemoteInput() {
1770         if (mHeadsUpRemoteInput != null) {
1771             mHeadsUpRemoteInput.close();
1772         }
1773         if (mExpandedRemoteInput != null) {
1774             mExpandedRemoteInput.close();
1775         }
1776     }
1777 
1778     public void setGroupMembershipManager(GroupMembershipManager groupMembershipManager) {
1779     }
1780 
1781     public void setRemoteInputController(RemoteInputController r) {
1782         mRemoteInputController = r;
1783     }
1784 
1785     public void setExpandClickListener(OnClickListener expandClickListener) {
1786         mExpandClickListener = expandClickListener;
1787     }
1788 
1789     public void updateExpandButtons(boolean expandable) {
1790         updateExpandButtonsDuringLayout(expandable, false /* duringLayout */);
1791     }
1792 
1793     private void updateExpandButtonsDuringLayout(boolean expandable, boolean duringLayout) {
1794         mExpandable = expandable;
1795         // if the expanded child has the same height as the collapsed one we hide it.
1796         if (mExpandedChild != null && mExpandedChild.getHeight() != 0) {
1797             if ((!mIsHeadsUp && !mHeadsUpAnimatingAway)
1798                     || mHeadsUpChild == null || !mContainingNotification.canShowHeadsUp()) {
1799                 if (mContractedChild == null
1800                         || mExpandedChild.getHeight() <= mContractedChild.getHeight()) {
1801                     expandable = false;
1802                 }
1803             } else if (mExpandedChild.getHeight() <= mHeadsUpChild.getHeight()) {
1804                 expandable = false;
1805             }
1806         }
1807         boolean requestLayout = duringLayout && mIsContentExpandable != expandable;
1808         if (mExpandedChild != null) {
1809             mExpandedWrapper.updateExpandability(expandable, mExpandClickListener, requestLayout);
1810         }
1811         if (mContractedChild != null) {
1812             mContractedWrapper.updateExpandability(expandable, mExpandClickListener, requestLayout);
1813         }
1814         if (mHeadsUpChild != null) {
1815             mHeadsUpWrapper.updateExpandability(expandable,  mExpandClickListener, requestLayout);
1816         }
1817         mIsContentExpandable = expandable;
1818     }
1819 
1820     /**
1821      * @return a view wrapper for one of the inflated states of the notification.
1822      */
1823     public NotificationViewWrapper getNotificationViewWrapper() {
1824         if (mContractedChild != null && mContractedWrapper != null) {
1825             return mContractedWrapper;
1826         }
1827         if (mExpandedChild != null && mExpandedWrapper != null) {
1828             return mExpandedWrapper;
1829         }
1830         if (mHeadsUpChild != null && mHeadsUpWrapper != null) {
1831             return mHeadsUpWrapper;
1832         }
1833         return null;
1834     }
1835 
1836     /** Shows the given feedback icon, or hides the icon if null. */
1837     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
1838         if (mContractedChild != null) {
1839             mContractedWrapper.setFeedbackIcon(icon);
1840         }
1841         if (mExpandedChild != null) {
1842             mExpandedWrapper.setFeedbackIcon(icon);
1843         }
1844         if (mHeadsUpChild != null) {
1845             mHeadsUpWrapper.setFeedbackIcon(icon);
1846         }
1847     }
1848 
1849     /** Sets whether the notification being displayed audibly alerted the user. */
1850     public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) {
1851         if (mContractedChild != null) {
1852             mContractedWrapper.setRecentlyAudiblyAlerted(audiblyAlerted);
1853         }
1854         if (mExpandedChild != null) {
1855             mExpandedWrapper.setRecentlyAudiblyAlerted(audiblyAlerted);
1856         }
1857         if (mHeadsUpChild != null) {
1858             mHeadsUpWrapper.setRecentlyAudiblyAlerted(audiblyAlerted);
1859         }
1860     }
1861 
1862     public void setContainingNotification(ExpandableNotificationRow containingNotification) {
1863         mContainingNotification = containingNotification;
1864     }
1865 
1866     public void requestSelectLayout(boolean needsAnimation) {
1867         selectLayout(needsAnimation, false);
1868     }
1869 
1870     public void reInflateViews() {
1871         if (mIsChildInGroup && mSingleLineView != null) {
1872             removeView(mSingleLineView);
1873             mSingleLineView = null;
1874             updateAllSingleLineViews();
1875         }
1876     }
1877 
1878     public void setUserExpanding(boolean userExpanding) {
1879         mUserExpanding = userExpanding;
1880         if (userExpanding) {
1881             mTransformationStartVisibleType = mVisibleType;
1882         } else {
1883             mTransformationStartVisibleType = VISIBLE_TYPE_NONE;
1884             mVisibleType = calculateVisibleType();
1885             updateViewVisibilities(mVisibleType);
1886             updateBackgroundColor(false);
1887         }
1888     }
1889 
1890     /**
1891      * Set by how much the single line view should be indented. Used when a overflow indicator is
1892      * present and only during measuring
1893      */
1894     public void setSingleLineWidthIndention(int singleLineWidthIndention) {
1895         if (singleLineWidthIndention != mSingleLineWidthIndention) {
1896             mSingleLineWidthIndention = singleLineWidthIndention;
1897             mContainingNotification.forceLayout();
1898             forceLayout();
1899         }
1900     }
1901 
1902     public HybridNotificationView getSingleLineView() {
1903         return mSingleLineView;
1904     }
1905 
1906     public void setRemoved() {
1907         if (mExpandedRemoteInput != null) {
1908             mExpandedRemoteInput.setRemoved();
1909         }
1910         if (mHeadsUpRemoteInput != null) {
1911             mHeadsUpRemoteInput.setRemoved();
1912         }
1913         if (mExpandedWrapper != null) {
1914             mExpandedWrapper.setRemoved();
1915         }
1916         if (mContractedWrapper != null) {
1917             mContractedWrapper.setRemoved();
1918         }
1919         if (mHeadsUpWrapper != null) {
1920             mHeadsUpWrapper.setRemoved();
1921         }
1922     }
1923 
1924     public void setContentHeightAnimating(boolean animating) {
1925         //TODO: It's odd that this does nothing when animating is true
1926         if (!animating) {
1927             mContentHeightAtAnimationStart = UNDEFINED;
1928         }
1929     }
1930 
1931     @VisibleForTesting
1932     boolean isAnimatingVisibleType() {
1933         return mAnimationStartVisibleType != VISIBLE_TYPE_NONE;
1934     }
1935 
1936     public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
1937         mHeadsUpAnimatingAway = headsUpAnimatingAway;
1938         selectLayout(false /* animate */, true /* force */);
1939     }
1940 
1941     public void setFocusOnVisibilityChange() {
1942         mFocusOnVisibilityChange = true;
1943     }
1944 
1945     @Override
1946     public void onVisibilityAggregated(boolean isVisible) {
1947         super.onVisibilityAggregated(isVisible);
1948         updateShownWrapper(mVisibleType);
1949         if (isVisible) {
1950             fireExpandedVisibleListenerIfVisible();
1951         }
1952     }
1953 
1954     /**
1955      * Sets a one-shot listener for when the expanded view becomes visible.
1956      *
1957      * This will fire the listener immediately if the expanded view is already visible.
1958      */
1959     public void setOnExpandedVisibleListener(Runnable r) {
1960         mExpandedVisibleListener = r;
1961         fireExpandedVisibleListenerIfVisible();
1962     }
1963 
1964     /**
1965      * Set a one-shot listener to run when a given content view becomes inactive.
1966      *
1967      * @param visibleType visible type corresponding to the content view to listen
1968      * @param listener runnable to run once when the content view becomes inactive
1969      */
1970     void performWhenContentInactive(int visibleType, Runnable listener) {
1971         View view = getViewForVisibleType(visibleType);
1972         // View is already inactive
1973         if (view == null || isContentViewInactive(visibleType)) {
1974             listener.run();
1975             return;
1976         }
1977         mOnContentViewInactiveListeners.put(view, listener);
1978     }
1979 
1980     /**
1981      * Remove content inactive listeners for a given content view . See
1982      * {@link #performWhenContentInactive}.
1983      *
1984      * @param visibleType visible type corresponding to the content type
1985      */
1986     void removeContentInactiveRunnable(int visibleType) {
1987         View view = getViewForVisibleType(visibleType);
1988         // View is already inactive
1989         if (view == null) {
1990             return;
1991         }
1992 
1993         mOnContentViewInactiveListeners.remove(view);
1994     }
1995 
1996     /**
1997      * Whether or not the content view is inactive.  This means it should not be visible
1998      * or the showing content as removing it would cause visual jank.
1999      *
2000      * @param visibleType visible type corresponding to the content view to be removed
2001      * @return true if the content view is inactive, false otherwise
2002      */
2003     public boolean isContentViewInactive(int visibleType) {
2004         View view = getViewForVisibleType(visibleType);
2005         return isContentViewInactive(view);
2006     }
2007 
2008     /**
2009      * Whether or not the content view is inactive.
2010      *
2011      * @param view view to see if its inactive
2012      * @return true if the view is inactive, false o/w
2013      */
2014     private boolean isContentViewInactive(View view) {
2015         if (view == null) {
2016             return true;
2017         }
2018         return !isShown()
2019                 || (view.getVisibility() != VISIBLE && getViewForVisibleType(mVisibleType) != view);
2020     }
2021 
2022     @Override
2023     protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
2024         super.onChildVisibilityChanged(child, oldVisibility, newVisibility);
2025         if (isContentViewInactive(child)) {
2026             Runnable listener = mOnContentViewInactiveListeners.remove(child);
2027             if (listener != null) {
2028                 listener.run();
2029             }
2030         }
2031     }
2032 
2033     public void setIsLowPriority(boolean isLowPriority) {
2034     }
2035 
2036     public boolean isDimmable() {
2037         return mContractedWrapper != null && mContractedWrapper.isDimmable();
2038     }
2039 
2040     /**
2041      * Should a single click be disallowed on this view when on the keyguard?
2042      */
2043     public boolean disallowSingleClick(float x, float y) {
2044         NotificationViewWrapper visibleWrapper = getVisibleWrapper(getVisibleType());
2045         if (visibleWrapper != null) {
2046             return visibleWrapper.disallowSingleClick(x, y);
2047         }
2048         return false;
2049     }
2050 
2051     public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
2052         boolean needsPaddings = shouldClipToRounding(getVisibleType(), topRounded, bottomRounded);
2053         if (mUserExpanding) {
2054              needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded,
2055                      bottomRounded);
2056         }
2057         return needsPaddings;
2058     }
2059 
2060     private boolean shouldClipToRounding(int visibleType, boolean topRounded,
2061             boolean bottomRounded) {
2062         NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType);
2063         if (visibleWrapper == null) {
2064             return false;
2065         }
2066         return visibleWrapper.shouldClipToRounding(topRounded, bottomRounded);
2067     }
2068 
2069     public CharSequence getActiveRemoteInputText() {
2070         if (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive()) {
2071             return mExpandedRemoteInput.getText();
2072         }
2073         if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive()) {
2074             return mHeadsUpRemoteInput.getText();
2075         }
2076         return null;
2077     }
2078 
2079     @Override
2080     public boolean dispatchTouchEvent(MotionEvent ev) {
2081         float y = ev.getY();
2082         // We still want to distribute touch events to the remote input even if it's outside the
2083         // view boundary. We're therefore manually dispatching these events to the remote view
2084         RemoteInputView riv = getRemoteInputForView(getViewForVisibleType(mVisibleType));
2085         if (riv != null && riv.getVisibility() == VISIBLE) {
2086             int inputStart = mUnrestrictedContentHeight - riv.getHeight();
2087             if (y <= mUnrestrictedContentHeight && y >= inputStart) {
2088                 ev.offsetLocation(0, -inputStart);
2089                 return riv.dispatchTouchEvent(ev);
2090             }
2091         }
2092         return super.dispatchTouchEvent(ev);
2093     }
2094 
2095     /**
2096      * Overridden to make sure touches to the reply action bar actually go through to this view
2097      */
2098     @Override
2099     public boolean pointInView(float localX, float localY, float slop) {
2100         float top = mClipTopAmount;
2101         float bottom = mUnrestrictedContentHeight;
2102         return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
2103                 localY < (bottom + slop);
2104     }
2105 
2106     private RemoteInputView getRemoteInputForView(View child) {
2107         if (child == mExpandedChild) {
2108             return mExpandedRemoteInput;
2109         } else if (child == mHeadsUpChild) {
2110             return mHeadsUpRemoteInput;
2111         }
2112         return null;
2113     }
2114 
2115     public int getExpandHeight() {
2116         int viewType;
2117         if (mExpandedChild != null) {
2118             viewType = VISIBLE_TYPE_EXPANDED;
2119         } else if (mContractedChild != null) {
2120             viewType = VISIBLE_TYPE_CONTRACTED;
2121         } else {
2122             return getMinHeight();
2123         }
2124         return getViewHeight(viewType) + getExtraRemoteInputHeight(mExpandedRemoteInput);
2125     }
2126 
2127     public int getHeadsUpHeight(boolean forceNoHeader) {
2128         int viewType;
2129         if (mHeadsUpChild != null) {
2130             viewType = VISIBLE_TYPE_HEADSUP;
2131         } else if (mContractedChild != null) {
2132             viewType = VISIBLE_TYPE_CONTRACTED;
2133         } else {
2134             return getMinHeight();
2135         }
2136         // The headsUp remote input quickly switches to the expanded one, so lets also include that
2137         // one
2138         return getViewHeight(viewType, forceNoHeader)
2139                 + getExtraRemoteInputHeight(mHeadsUpRemoteInput)
2140                 + getExtraRemoteInputHeight(mExpandedRemoteInput);
2141     }
2142 
2143     public void setRemoteInputVisible(boolean remoteInputVisible) {
2144         mRemoteInputVisible = remoteInputVisible;
2145         setClipChildren(!remoteInputVisible);
2146         setActionsImportanceForAccessibility(
2147                 remoteInputVisible ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
2148                         : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
2149     }
2150 
2151     private void setActionsImportanceForAccessibility(int mode) {
2152         if (mExpandedChild != null) {
2153             setActionsImportanceForAccessibility(mode, mExpandedChild);
2154         }
2155         if (mHeadsUpChild != null) {
2156             setActionsImportanceForAccessibility(mode, mHeadsUpChild);
2157         }
2158     }
2159 
2160     private void setActionsImportanceForAccessibility(int mode, View child) {
2161         View actionsCandidate = child.findViewById(com.android.internal.R.id.actions);
2162         if (actionsCandidate != null) {
2163             actionsCandidate.setImportantForAccessibility(mode);
2164         }
2165     }
2166 
2167     @Override
2168     public void setClipChildren(boolean clipChildren) {
2169         clipChildren = clipChildren && !mRemoteInputVisible;
2170         super.setClipChildren(clipChildren);
2171     }
2172 
2173     public void setHeaderVisibleAmount(float headerVisibleAmount) {
2174         if (mContractedWrapper != null) {
2175             mContractedWrapper.setHeaderVisibleAmount(headerVisibleAmount);
2176         }
2177         if (mHeadsUpWrapper != null) {
2178             mHeadsUpWrapper.setHeaderVisibleAmount(headerVisibleAmount);
2179         }
2180         if (mExpandedWrapper != null) {
2181             mExpandedWrapper.setHeaderVisibleAmount(headerVisibleAmount);
2182         }
2183     }
2184 
2185     public void dump(PrintWriter pw, String[] args) {
2186         pw.print("contentView visibility: " + getVisibility());
2187         pw.print(", alpha: " + getAlpha());
2188         pw.print(", clipBounds: " + getClipBounds());
2189         pw.print(", contentHeight: " + mContentHeight);
2190         pw.print(", visibleType: " + mVisibleType);
2191         View view = getViewForVisibleType(mVisibleType);
2192         pw.print(", visibleView ");
2193         if (view != null) {
2194             pw.print(" visibility: " + view.getVisibility());
2195             pw.print(", alpha: " + view.getAlpha());
2196             pw.print(", clipBounds: " + view.getClipBounds());
2197         } else {
2198             pw.print("null");
2199         }
2200         pw.println();
2201 
2202         if (INCLUDE_HEIGHTS_TO_DUMP) {
2203             dumpContentDimensions(DumpUtilsKt.asIndenting(pw));
2204         }
2205 
2206         pw.println("mBubblesEnabledForUser: " + mBubblesEnabledForUser);
2207 
2208         pw.print("RemoteInputViews { ");
2209         pw.print(" visibleType: " + mVisibleType);
2210         if (mHeadsUpRemoteInputController != null) {
2211             pw.print(", headsUpRemoteInputController.isActive: "
2212                     + mHeadsUpRemoteInputController.isActive());
2213         } else {
2214             pw.print(", headsUpRemoteInputController: null");
2215         }
2216 
2217         if (mExpandedRemoteInputController != null) {
2218             pw.print(", expandedRemoteInputController.isActive: "
2219                     + mExpandedRemoteInputController.isActive());
2220         } else {
2221             pw.print(", expandedRemoteInputController: null");
2222         }
2223         pw.println(" }");
2224     }
2225 
2226     private String visibleTypeToString(int visibleType) {
2227         return switch (visibleType) {
2228             case VISIBLE_TYPE_CONTRACTED -> "CONTRACTED";
2229             case VISIBLE_TYPE_EXPANDED -> "EXPANDED";
2230             case VISIBLE_TYPE_HEADSUP -> "HEADSUP";
2231             case VISIBLE_TYPE_SINGLELINE -> "SINGLELINE";
2232             default -> "NONE";
2233         };
2234     }
2235 
2236     /** Add content views to dump */
2237     private void dumpContentDimensions(IndentingPrintWriter pw) {
2238         pw.print("ContentDimensions: ");
2239         pw.print("visibleType(String)", visibleTypeToString(mVisibleType));
2240         pw.print("measured width", getMeasuredWidth());
2241         pw.print("measured height", getMeasuredHeight());
2242         pw.print("maxHeight", getMaxHeight());
2243         pw.print("minHeight", getMinHeight());
2244         pw.println();
2245         pw.println("ChildViews:");
2246         DumpUtilsKt.withIncreasedIndent(pw, () -> {
2247             final View contractedChild = mContractedChild;
2248             if (contractedChild != null) {
2249                 dumpChildViewDimensions(pw, contractedChild, "Contracted Child:");
2250                 pw.println();
2251             }
2252 
2253             final View expandedChild = mExpandedChild;
2254             if (expandedChild != null) {
2255                 dumpChildViewDimensions(pw, expandedChild, "Expanded Child:");
2256                 pw.println();
2257             }
2258 
2259             final View headsUpChild = mHeadsUpChild;
2260             if (headsUpChild != null) {
2261                 dumpChildViewDimensions(pw, headsUpChild, "HeadsUp Child:");
2262                 pw.println();
2263             }
2264             final View singleLineView = mSingleLineView;
2265             if (singleLineView != null) {
2266                 dumpChildViewDimensions(pw, singleLineView, "Single Line  View:");
2267                 pw.println();
2268             }
2269 
2270         });
2271         final int expandedRemoteInputHeight = getExtraRemoteInputHeight(mExpandedRemoteInput);
2272         final int headsUpRemoteInputHeight = getExtraRemoteInputHeight(mHeadsUpRemoteInput);
2273         pw.print("expandedRemoteInputHeight", expandedRemoteInputHeight);
2274         pw.print("headsUpRemoteInputHeight", headsUpRemoteInputHeight);
2275         pw.println();
2276     }
2277 
2278     private void dumpChildViewDimensions(IndentingPrintWriter pw, View view,
2279             String name) {
2280         pw.print(name + " ");
2281         DumpUtilsKt.withIncreasedIndent(pw, () -> {
2282             pw.print("width", view.getWidth());
2283             pw.print("height", view.getHeight());
2284             pw.print("measuredWidth", view.getMeasuredWidth());
2285             pw.print("measuredHeight", view.getMeasuredHeight());
2286         });
2287     }
2288 
2289     /** Add any existing SmartReplyView to the dump */
2290     public void dumpSmartReplies(IndentingPrintWriter pw) {
2291         if (mHeadsUpSmartReplyView != null) {
2292             pw.println("HeadsUp SmartReplyView:");
2293             pw.increaseIndent();
2294             mHeadsUpSmartReplyView.dump(pw);
2295             pw.decreaseIndent();
2296         }
2297         if (mExpandedSmartReplyView != null) {
2298             pw.println("Expanded SmartReplyView:");
2299             pw.increaseIndent();
2300             mExpandedSmartReplyView.dump(pw);
2301             pw.decreaseIndent();
2302         }
2303     }
2304 
2305     public RemoteInputView getExpandedRemoteInput() {
2306         return mExpandedRemoteInput;
2307     }
2308 
2309     /**
2310      * @return get the transformation target of the shelf, which usually is the icon
2311      */
2312     public View getShelfTransformationTarget() {
2313         NotificationViewWrapper visibleWrapper = getVisibleWrapper(mVisibleType);
2314         if (visibleWrapper != null) {
2315             return visibleWrapper.getShelfTransformationTarget();
2316         }
2317         return null;
2318     }
2319 
2320     public int getOriginalIconColor() {
2321         NotificationViewWrapper visibleWrapper = getVisibleWrapper(mVisibleType);
2322         if (visibleWrapper != null) {
2323             return visibleWrapper.getOriginalIconColor();
2324         }
2325         return Notification.COLOR_INVALID;
2326     }
2327 
2328     /**
2329      * Delegate the faded state to the notification content views which actually
2330      * need to have overlapping contents render precisely.
2331      */
2332     @Override
2333     public void setNotificationFaded(boolean faded) {
2334         if (mContractedWrapper != null) {
2335             mContractedWrapper.setNotificationFaded(faded);
2336         }
2337         if (mHeadsUpWrapper != null) {
2338             mHeadsUpWrapper.setNotificationFaded(faded);
2339         }
2340         if (mExpandedWrapper != null) {
2341             mExpandedWrapper.setNotificationFaded(faded);
2342         }
2343         if (mSingleLineView != null) {
2344             mSingleLineView.setNotificationFaded(faded);
2345         }
2346     }
2347 
2348     /**
2349      * @return true if a visible view has a remote input active, as this requires that the entire
2350      * row report that it has overlapping rendering.
2351      */
2352     public boolean requireRowToHaveOverlappingRendering() {
2353         // This inexpensive check is done on both states to avoid state invalidating the result.
2354         if (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive()) {
2355             return true;
2356         }
2357         if (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive()) {
2358             return true;
2359         }
2360         return false;
2361     }
2362 
2363     /**
2364      * Starts and stops animations in the underlying views.
2365      * Avoids restarting the animations by checking whether they're already running first.
2366      * Return value is used for testing.
2367      *
2368      * @param running whether to start animations running, or stop them.
2369      * @return true if the state of animations changed.
2370      */
2371     public boolean setContentAnimationRunning(boolean running) {
2372         boolean stateChangeRequired = (running != mContentAnimating);
2373         if (stateChangeRequired) {
2374             // Starts or stops the animations in the potential views.
2375             if (mContractedWrapper != null) {
2376                 mContractedWrapper.setAnimationsRunning(running);
2377             }
2378             if (mExpandedWrapper != null) {
2379                 mExpandedWrapper.setAnimationsRunning(running);
2380             }
2381             if (mHeadsUpWrapper != null) {
2382                 mHeadsUpWrapper.setAnimationsRunning(running);
2383             }
2384             // Updates the state tracker.
2385             mContentAnimating = running;
2386             return true;
2387         }
2388         return false;
2389     }
2390 
2391     public void setNotificationWhen(long whenMillis) {
2392         NotificationViewWrapper wrapper = getNotificationViewWrapper();
2393         if (wrapper instanceof NotificationHeaderViewWrapper headerViewWrapper) {
2394             headerViewWrapper.setNotificationWhen(whenMillis);
2395         }
2396     }
2397 
2398     private static class RemoteInputViewData {
2399         @Nullable RemoteInputView mView;
2400         @Nullable RemoteInputViewController mController;
2401     }
2402 
2403     @VisibleForTesting
2404     protected NotificationViewWrapper getContractedWrapper() {
2405         return mContractedWrapper;
2406     }
2407 
2408     @VisibleForTesting
2409     protected NotificationViewWrapper getExpandedWrapper() {
2410         return mExpandedWrapper;
2411     }
2412 
2413     @VisibleForTesting
2414     protected NotificationViewWrapper getHeadsUpWrapper() {
2415         return mHeadsUpWrapper;
2416     }
2417 
2418     @VisibleForTesting
2419     protected void setContractedWrapper(NotificationViewWrapper contractedWrapper) {
2420         mContractedWrapper = contractedWrapper;
2421     }
2422     @VisibleForTesting
2423     protected void setExpandedWrapper(NotificationViewWrapper expandedWrapper) {
2424         mExpandedWrapper = expandedWrapper;
2425     }
2426     @VisibleForTesting
2427     protected void setHeadsUpWrapper(NotificationViewWrapper headsUpWrapper) {
2428         mHeadsUpWrapper = headsUpWrapper;
2429     }
2430 
2431     @VisibleForTesting
2432     protected void setAnimationStartVisibleType(int animationStartVisibleType) {
2433         mAnimationStartVisibleType = animationStartVisibleType;
2434     }
2435 
2436     @Override
2437     protected void dispatchDraw(Canvas canvas) {
2438         try {
2439             super.dispatchDraw(canvas);
2440         } catch (Exception e) {
2441             // Catch draw exceptions that may be caused by RemoteViews
2442             Log.e(TAG, "Drawing view failed: " + e);
2443             cancelNotification(e);
2444         }
2445     }
2446 
2447     private void cancelNotification(Exception exception) {
2448         try {
2449             setVisibility(GONE);
2450             final StatusBarNotification sbn = mNotificationEntry.getSbn();
2451             if (mStatusBarService != null) {
2452                 // report notification inflation errors back up
2453                 // to notification delegates
2454                 mStatusBarService.onNotificationError(
2455                         sbn.getPackageName(),
2456                         sbn.getTag(),
2457                         sbn.getId(),
2458                         sbn.getUid(),
2459                         sbn.getInitialPid(),
2460                         exception.getMessage(),
2461                         sbn.getUser().getIdentifier());
2462             }
2463         } catch (RemoteException ex) {
2464             Log.e(TAG, "cancelNotification failed: " + ex);
2465         }
2466     }
2467 }
2468