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