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