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