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