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;
18 
19 import android.app.Notification;
20 import android.app.PendingIntent;
21 import android.app.RemoteInput;
22 import android.content.Context;
23 import android.graphics.Rect;
24 import android.os.Build;
25 import android.service.notification.StatusBarNotification;
26 import android.util.AttributeSet;
27 import android.view.NotificationHeaderView;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.ViewTreeObserver;
31 import android.widget.FrameLayout;
32 import android.widget.ImageView;
33 
34 import com.android.internal.util.NotificationColorUtil;
35 import com.android.systemui.R;
36 import com.android.systemui.statusbar.notification.HybridNotificationView;
37 import com.android.systemui.statusbar.notification.HybridGroupManager;
38 import com.android.systemui.statusbar.notification.NotificationCustomViewWrapper;
39 import com.android.systemui.statusbar.notification.NotificationUtils;
40 import com.android.systemui.statusbar.notification.NotificationViewWrapper;
41 import com.android.systemui.statusbar.phone.NotificationGroupManager;
42 import com.android.systemui.statusbar.policy.RemoteInputView;
43 
44 /**
45  * A frame layout containing the actual payload of the notification, including the contracted,
46  * expanded and heads up layout. This class is responsible for clipping the content and and
47  * switching between the expanded, contracted and the heads up view depending on its clipped size.
48  */
49 public class NotificationContentView extends FrameLayout {
50 
51     private static final int VISIBLE_TYPE_CONTRACTED = 0;
52     private static final int VISIBLE_TYPE_EXPANDED = 1;
53     private static final int VISIBLE_TYPE_HEADSUP = 2;
54     private static final int VISIBLE_TYPE_SINGLELINE = 3;
55     public static final int UNDEFINED = -1;
56 
57     private final Rect mClipBounds = new Rect();
58     private final int mMinContractedHeight;
59     private final int mNotificationContentMarginEnd;
60 
61     private View mContractedChild;
62     private View mExpandedChild;
63     private View mHeadsUpChild;
64     private HybridNotificationView mSingleLineView;
65 
66     private RemoteInputView mExpandedRemoteInput;
67     private RemoteInputView mHeadsUpRemoteInput;
68 
69     private NotificationViewWrapper mContractedWrapper;
70     private NotificationViewWrapper mExpandedWrapper;
71     private NotificationViewWrapper mHeadsUpWrapper;
72     private HybridGroupManager mHybridGroupManager;
73     private int mClipTopAmount;
74     private int mContentHeight;
75     private int mVisibleType = VISIBLE_TYPE_CONTRACTED;
76     private boolean mDark;
77     private boolean mAnimate;
78     private boolean mIsHeadsUp;
79     private boolean mShowingLegacyBackground;
80     private boolean mIsChildInGroup;
81     private int mSmallHeight;
82     private int mHeadsUpHeight;
83     private int mNotificationMaxHeight;
84     private StatusBarNotification mStatusBarNotification;
85     private NotificationGroupManager mGroupManager;
86     private RemoteInputController mRemoteInputController;
87 
88     private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener
89             = new ViewTreeObserver.OnPreDrawListener() {
90         @Override
91         public boolean onPreDraw() {
92             // We need to post since we don't want the notification to animate on the very first
93             // frame
94             post(new Runnable() {
95                 @Override
96                 public void run() {
97                     mAnimate = true;
98                 }
99             });
100             getViewTreeObserver().removeOnPreDrawListener(this);
101             return true;
102         }
103     };
104 
105     private OnClickListener mExpandClickListener;
106     private boolean mBeforeN;
107     private boolean mExpandable;
108     private boolean mClipToActualHeight = true;
109     private ExpandableNotificationRow mContainingNotification;
110     /** The visible type at the start of a touch driven transformation */
111     private int mTransformationStartVisibleType;
112     /** The visible type at the start of an animation driven transformation */
113     private int mAnimationStartVisibleType = UNDEFINED;
114     private boolean mUserExpanding;
115     private int mSingleLineWidthIndention;
116     private boolean mForceSelectNextLayout = true;
117     private PendingIntent mPreviousExpandedRemoteInputIntent;
118     private PendingIntent mPreviousHeadsUpRemoteInputIntent;
119 
120     private int mContentHeightAtAnimationStart = UNDEFINED;
121     private boolean mFocusOnVisibilityChange;
122     private boolean mHeadsupDisappearRunning;
123 
124 
NotificationContentView(Context context, AttributeSet attrs)125     public NotificationContentView(Context context, AttributeSet attrs) {
126         super(context, attrs);
127         mHybridGroupManager = new HybridGroupManager(getContext(), this);
128         mMinContractedHeight = getResources().getDimensionPixelSize(
129                 R.dimen.min_notification_layout_height);
130         mNotificationContentMarginEnd = getResources().getDimensionPixelSize(
131                 com.android.internal.R.dimen.notification_content_margin_end);
132         reset();
133     }
134 
setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight)135     public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight) {
136         mSmallHeight = smallHeight;
137         mHeadsUpHeight = headsUpMaxHeight;
138         mNotificationMaxHeight = maxHeight;
139     }
140 
141     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)142     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
143         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
144         boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
145         boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
146         int maxSize = Integer.MAX_VALUE;
147         int width = MeasureSpec.getSize(widthMeasureSpec);
148         if (hasFixedHeight || isHeightLimited) {
149             maxSize = MeasureSpec.getSize(heightMeasureSpec);
150         }
151         int maxChildHeight = 0;
152         if (mExpandedChild != null) {
153             int size = Math.min(maxSize, mNotificationMaxHeight);
154             ViewGroup.LayoutParams layoutParams = mExpandedChild.getLayoutParams();
155             if (layoutParams.height >= 0) {
156                 // An actual height is set
157                 size = Math.min(maxSize, layoutParams.height);
158             }
159             int spec = size == Integer.MAX_VALUE
160                     ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
161                     : MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
162             mExpandedChild.measure(widthMeasureSpec, spec);
163             maxChildHeight = Math.max(maxChildHeight, mExpandedChild.getMeasuredHeight());
164         }
165         if (mContractedChild != null) {
166             int heightSpec;
167             int size = Math.min(maxSize, mSmallHeight);
168             if (shouldContractedBeFixedSize()) {
169                 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
170             } else {
171                 heightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
172             }
173             mContractedChild.measure(widthMeasureSpec, heightSpec);
174             int measuredHeight = mContractedChild.getMeasuredHeight();
175             if (measuredHeight < mMinContractedHeight) {
176                 heightSpec = MeasureSpec.makeMeasureSpec(mMinContractedHeight, MeasureSpec.EXACTLY);
177                 mContractedChild.measure(widthMeasureSpec, heightSpec);
178             }
179             maxChildHeight = Math.max(maxChildHeight, measuredHeight);
180             if (updateContractedHeaderWidth()) {
181                 mContractedChild.measure(widthMeasureSpec, heightSpec);
182             }
183             if (mExpandedChild != null
184                     && mContractedChild.getMeasuredHeight() > mExpandedChild.getMeasuredHeight()) {
185                 // the Expanded child is smaller then the collapsed. Let's remeasure it.
186                 heightSpec = MeasureSpec.makeMeasureSpec(mContractedChild.getMeasuredHeight(),
187                         MeasureSpec.EXACTLY);
188                 mExpandedChild.measure(widthMeasureSpec, heightSpec);
189             }
190         }
191         if (mHeadsUpChild != null) {
192             int size = Math.min(maxSize, mHeadsUpHeight);
193             ViewGroup.LayoutParams layoutParams = mHeadsUpChild.getLayoutParams();
194             if (layoutParams.height >= 0) {
195                 // An actual height is set
196                 size = Math.min(size, layoutParams.height);
197             }
198             mHeadsUpChild.measure(widthMeasureSpec,
199                     MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST));
200             maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight());
201         }
202         if (mSingleLineView != null) {
203             int singleLineWidthSpec = widthMeasureSpec;
204             if (mSingleLineWidthIndention != 0
205                     && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
206                 singleLineWidthSpec = MeasureSpec.makeMeasureSpec(
207                         width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(),
208                         MeasureSpec.AT_MOST);
209             }
210             mSingleLineView.measure(singleLineWidthSpec,
211                     MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST));
212             maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight());
213         }
214         int ownHeight = Math.min(maxChildHeight, maxSize);
215         setMeasuredDimension(width, ownHeight);
216     }
217 
updateContractedHeaderWidth()218     private boolean updateContractedHeaderWidth() {
219         // We need to update the expanded and the collapsed header to have exactly the same with to
220         // have the expand buttons laid out at the same location.
221         NotificationHeaderView contractedHeader = mContractedWrapper.getNotificationHeader();
222         if (contractedHeader != null) {
223             if (mExpandedChild != null
224                     && mExpandedWrapper.getNotificationHeader() != null) {
225                 NotificationHeaderView expandedHeader = mExpandedWrapper.getNotificationHeader();
226                 int expandedSize = expandedHeader.getMeasuredWidth()
227                         - expandedHeader.getPaddingEnd();
228                 int collapsedSize = contractedHeader.getMeasuredWidth()
229                         - expandedHeader.getPaddingEnd();
230                 if (expandedSize != collapsedSize) {
231                     int paddingEnd = contractedHeader.getMeasuredWidth() - expandedSize;
232                     contractedHeader.setPadding(
233                             contractedHeader.isLayoutRtl()
234                                     ? paddingEnd
235                                     : contractedHeader.getPaddingLeft(),
236                             contractedHeader.getPaddingTop(),
237                             contractedHeader.isLayoutRtl()
238                                     ? contractedHeader.getPaddingLeft()
239                                     : paddingEnd,
240                             contractedHeader.getPaddingBottom());
241                     contractedHeader.setShowWorkBadgeAtEnd(true);
242                     return true;
243                 }
244             } else {
245                 int paddingEnd = mNotificationContentMarginEnd;
246                 if (contractedHeader.getPaddingEnd() != paddingEnd) {
247                     contractedHeader.setPadding(
248                             contractedHeader.isLayoutRtl()
249                                     ? paddingEnd
250                                     : contractedHeader.getPaddingLeft(),
251                             contractedHeader.getPaddingTop(),
252                             contractedHeader.isLayoutRtl()
253                                     ? contractedHeader.getPaddingLeft()
254                                     : paddingEnd,
255                             contractedHeader.getPaddingBottom());
256                     contractedHeader.setShowWorkBadgeAtEnd(false);
257                     return true;
258                 }
259             }
260         }
261         return false;
262     }
263 
shouldContractedBeFixedSize()264     private boolean shouldContractedBeFixedSize() {
265         return mBeforeN && mContractedWrapper instanceof NotificationCustomViewWrapper;
266     }
267 
268     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)269     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
270         int previousHeight = 0;
271         if (mExpandedChild != null) {
272             previousHeight = mExpandedChild.getHeight();
273         }
274         super.onLayout(changed, left, top, right, bottom);
275         if (previousHeight != 0 && mExpandedChild.getHeight() != previousHeight) {
276             mContentHeightAtAnimationStart = previousHeight;
277         }
278         updateClipping();
279         invalidateOutline();
280         selectLayout(false /* animate */, mForceSelectNextLayout /* force */);
281         mForceSelectNextLayout = false;
282         updateExpandButtons(mExpandable);
283     }
284 
285     @Override
onAttachedToWindow()286     protected void onAttachedToWindow() {
287         super.onAttachedToWindow();
288         updateVisibility();
289     }
290 
reset()291     public void reset() {
292         if (mContractedChild != null) {
293             mContractedChild.animate().cancel();
294             removeView(mContractedChild);
295         }
296         mPreviousExpandedRemoteInputIntent = null;
297         if (mExpandedRemoteInput != null) {
298             mExpandedRemoteInput.onNotificationUpdateOrReset();
299             if (mExpandedRemoteInput.isActive()) {
300                 mPreviousExpandedRemoteInputIntent = mExpandedRemoteInput.getPendingIntent();
301             }
302         }
303         if (mExpandedChild != null) {
304             mExpandedChild.animate().cancel();
305             removeView(mExpandedChild);
306             mExpandedRemoteInput = null;
307         }
308         mPreviousHeadsUpRemoteInputIntent = null;
309         if (mHeadsUpRemoteInput != null) {
310             mHeadsUpRemoteInput.onNotificationUpdateOrReset();
311             if (mHeadsUpRemoteInput.isActive()) {
312                 mPreviousHeadsUpRemoteInputIntent = mHeadsUpRemoteInput.getPendingIntent();
313             }
314         }
315         if (mHeadsUpChild != null) {
316             mHeadsUpChild.animate().cancel();
317             removeView(mHeadsUpChild);
318             mHeadsUpRemoteInput = null;
319         }
320         mContractedChild = null;
321         mExpandedChild = null;
322         mHeadsUpChild = null;
323     }
324 
getContractedChild()325     public View getContractedChild() {
326         return mContractedChild;
327     }
328 
getExpandedChild()329     public View getExpandedChild() {
330         return mExpandedChild;
331     }
332 
getHeadsUpChild()333     public View getHeadsUpChild() {
334         return mHeadsUpChild;
335     }
336 
setContractedChild(View child)337     public void setContractedChild(View child) {
338         if (mContractedChild != null) {
339             mContractedChild.animate().cancel();
340             removeView(mContractedChild);
341         }
342         addView(child);
343         mContractedChild = child;
344         mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child,
345                 mContainingNotification);
346         mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
347     }
348 
setExpandedChild(View child)349     public void setExpandedChild(View child) {
350         if (mExpandedChild != null) {
351             mExpandedChild.animate().cancel();
352             removeView(mExpandedChild);
353         }
354         addView(child);
355         mExpandedChild = child;
356         mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child,
357                 mContainingNotification);
358     }
359 
setHeadsUpChild(View child)360     public void setHeadsUpChild(View child) {
361         if (mHeadsUpChild != null) {
362             mHeadsUpChild.animate().cancel();
363             removeView(mHeadsUpChild);
364         }
365         addView(child);
366         mHeadsUpChild = child;
367         mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child,
368                 mContainingNotification);
369     }
370 
371     @Override
onVisibilityChanged(View changedView, int visibility)372     protected void onVisibilityChanged(View changedView, int visibility) {
373         super.onVisibilityChanged(changedView, visibility);
374         updateVisibility();
375     }
376 
updateVisibility()377     private void updateVisibility() {
378         setVisible(isShown());
379     }
380 
381     @Override
onDetachedFromWindow()382     protected void onDetachedFromWindow() {
383         super.onDetachedFromWindow();
384         getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
385     }
386 
setVisible(final boolean isVisible)387     private void setVisible(final boolean isVisible) {
388         if (isVisible) {
389             // This call can happen multiple times, but removing only removes a single one.
390             // We therefore need to remove the old one.
391             getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
392             // We only animate if we are drawn at least once, otherwise the view might animate when
393             // it's shown the first time
394             getViewTreeObserver().addOnPreDrawListener(mEnableAnimationPredrawListener);
395         } else {
396             getViewTreeObserver().removeOnPreDrawListener(mEnableAnimationPredrawListener);
397             mAnimate = false;
398         }
399     }
400 
focusExpandButtonIfNecessary()401     private void focusExpandButtonIfNecessary() {
402         if (mFocusOnVisibilityChange) {
403             NotificationHeaderView header = getVisibleNotificationHeader();
404             if (header != null) {
405                 ImageView expandButton = header.getExpandButton();
406                 if (expandButton != null) {
407                     expandButton.requestAccessibilityFocus();
408                 }
409             }
410             mFocusOnVisibilityChange = false;
411         }
412     }
413 
setContentHeight(int contentHeight)414     public void setContentHeight(int contentHeight) {
415         mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());
416         selectLayout(mAnimate /* animate */, false /* force */);
417 
418         int minHeightHint = getMinContentHeightHint();
419 
420         NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
421         if (wrapper != null) {
422             wrapper.setContentHeight(mContentHeight, minHeightHint);
423         }
424 
425         wrapper = getVisibleWrapper(mTransformationStartVisibleType);
426         if (wrapper != null) {
427             wrapper.setContentHeight(mContentHeight, minHeightHint);
428         }
429 
430         updateClipping();
431         invalidateOutline();
432     }
433 
434     /**
435      * @return the minimum apparent height that the wrapper should allow for the purpose
436      *         of aligning elements at the bottom edge. If this is larger than the content
437      *         height, the notification is clipped instead of being further shrunk.
438      */
getMinContentHeightHint()439     private int getMinContentHeightHint() {
440         if (mIsChildInGroup && isVisibleOrTransitioning(VISIBLE_TYPE_SINGLELINE)) {
441             return mContext.getResources().getDimensionPixelSize(
442                         com.android.internal.R.dimen.notification_action_list_height);
443         }
444 
445         // Transition between heads-up & expanded, or pinned.
446         if (mHeadsUpChild != null && mExpandedChild != null) {
447             boolean transitioningBetweenHunAndExpanded =
448                     isTransitioningFromTo(VISIBLE_TYPE_HEADSUP, VISIBLE_TYPE_EXPANDED) ||
449                     isTransitioningFromTo(VISIBLE_TYPE_EXPANDED, VISIBLE_TYPE_HEADSUP);
450             boolean pinned = !isVisibleOrTransitioning(VISIBLE_TYPE_CONTRACTED)
451                     && (mIsHeadsUp || mHeadsupDisappearRunning);
452             if (transitioningBetweenHunAndExpanded || pinned) {
453                 return Math.min(mHeadsUpChild.getHeight(), mExpandedChild.getHeight());
454             }
455         }
456 
457         // Size change of the expanded version
458         if ((mVisibleType == VISIBLE_TYPE_EXPANDED) && mContentHeightAtAnimationStart >= 0
459                 && mExpandedChild != null) {
460             return Math.min(mContentHeightAtAnimationStart, mExpandedChild.getHeight());
461         }
462 
463         int hint;
464         if (mHeadsUpChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_HEADSUP)) {
465             hint = mHeadsUpChild.getHeight();
466         } else if (mExpandedChild != null) {
467             hint = mExpandedChild.getHeight();
468         } else {
469             hint = mContractedChild.getHeight() + mContext.getResources().getDimensionPixelSize(
470                     com.android.internal.R.dimen.notification_action_list_height);
471         }
472 
473         if (mExpandedChild != null && isVisibleOrTransitioning(VISIBLE_TYPE_EXPANDED)) {
474             hint = Math.min(hint, mExpandedChild.getHeight());
475         }
476         return hint;
477     }
478 
isTransitioningFromTo(int from, int to)479     private boolean isTransitioningFromTo(int from, int to) {
480         return (mTransformationStartVisibleType == from || mAnimationStartVisibleType == from)
481                 && mVisibleType == to;
482     }
483 
isVisibleOrTransitioning(int type)484     private boolean isVisibleOrTransitioning(int type) {
485         return mVisibleType == type || mTransformationStartVisibleType == type
486                 || mAnimationStartVisibleType == type;
487     }
488 
updateContentTransformation()489     private void updateContentTransformation() {
490         int visibleType = calculateVisibleType();
491         if (visibleType != mVisibleType) {
492             // A new transformation starts
493             mTransformationStartVisibleType = mVisibleType;
494             final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
495             final TransformableView hiddenView = getTransformableViewForVisibleType(
496                     mTransformationStartVisibleType);
497             shownView.transformFrom(hiddenView, 0.0f);
498             getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
499             hiddenView.transformTo(shownView, 0.0f);
500             mVisibleType = visibleType;
501             updateBackgroundColor(true /* animate */);
502         }
503         if (mForceSelectNextLayout) {
504             forceUpdateVisibilities();
505         }
506         if (mTransformationStartVisibleType != UNDEFINED
507                 && mVisibleType != mTransformationStartVisibleType
508                 && getViewForVisibleType(mTransformationStartVisibleType) != null) {
509             final TransformableView shownView = getTransformableViewForVisibleType(mVisibleType);
510             final TransformableView hiddenView = getTransformableViewForVisibleType(
511                     mTransformationStartVisibleType);
512             float transformationAmount = calculateTransformationAmount();
513             shownView.transformFrom(hiddenView, transformationAmount);
514             hiddenView.transformTo(shownView, transformationAmount);
515             updateBackgroundTransformation(transformationAmount);
516         } else {
517             updateViewVisibilities(visibleType);
518             updateBackgroundColor(false);
519         }
520     }
521 
updateBackgroundTransformation(float transformationAmount)522     private void updateBackgroundTransformation(float transformationAmount) {
523         int endColor = getBackgroundColor(mVisibleType);
524         int startColor = getBackgroundColor(mTransformationStartVisibleType);
525         if (endColor != startColor) {
526             if (startColor == 0) {
527                 startColor = mContainingNotification.getBackgroundColorWithoutTint();
528             }
529             if (endColor == 0) {
530                 endColor = mContainingNotification.getBackgroundColorWithoutTint();
531             }
532             endColor = NotificationUtils.interpolateColors(startColor, endColor,
533                     transformationAmount);
534         }
535         mContainingNotification.updateBackgroundAlpha(transformationAmount);
536         mContainingNotification.setContentBackground(endColor, false, this);
537     }
538 
calculateTransformationAmount()539     private float calculateTransformationAmount() {
540         int startHeight = getViewForVisibleType(mTransformationStartVisibleType).getHeight();
541         int endHeight = getViewForVisibleType(mVisibleType).getHeight();
542         int progress = Math.abs(mContentHeight - startHeight);
543         int totalDistance = Math.abs(endHeight - startHeight);
544         float amount = (float) progress / (float) totalDistance;
545         return Math.min(1.0f, amount);
546     }
547 
getContentHeight()548     public int getContentHeight() {
549         return mContentHeight;
550     }
551 
getMaxHeight()552     public int getMaxHeight() {
553         if (mExpandedChild != null) {
554             return mExpandedChild.getHeight();
555         } else if (mIsHeadsUp && mHeadsUpChild != null) {
556             return mHeadsUpChild.getHeight();
557         }
558         return mContractedChild.getHeight();
559     }
560 
getMinHeight()561     public int getMinHeight() {
562         return getMinHeight(false /* likeGroupExpanded */);
563     }
564 
getMinHeight(boolean likeGroupExpanded)565     public int getMinHeight(boolean likeGroupExpanded) {
566         if (likeGroupExpanded || !mIsChildInGroup || isGroupExpanded()) {
567             return mContractedChild.getHeight();
568         } else {
569             return mSingleLineView.getHeight();
570         }
571     }
572 
isGroupExpanded()573     private boolean isGroupExpanded() {
574         return mGroupManager.isGroupExpanded(mStatusBarNotification);
575     }
576 
setClipTopAmount(int clipTopAmount)577     public void setClipTopAmount(int clipTopAmount) {
578         mClipTopAmount = clipTopAmount;
579         updateClipping();
580     }
581 
updateClipping()582     private void updateClipping() {
583         if (mClipToActualHeight) {
584             mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
585             setClipBounds(mClipBounds);
586         } else {
587             setClipBounds(null);
588         }
589     }
590 
setClipToActualHeight(boolean clipToActualHeight)591     public void setClipToActualHeight(boolean clipToActualHeight) {
592         mClipToActualHeight = clipToActualHeight;
593         updateClipping();
594     }
595 
selectLayout(boolean animate, boolean force)596     private void selectLayout(boolean animate, boolean force) {
597         if (mContractedChild == null) {
598             return;
599         }
600         if (mUserExpanding) {
601             updateContentTransformation();
602         } else {
603             int visibleType = calculateVisibleType();
604             boolean changedType = visibleType != mVisibleType;
605             if (changedType || force) {
606                 View visibleView = getViewForVisibleType(visibleType);
607                 if (visibleView != null) {
608                     visibleView.setVisibility(VISIBLE);
609                     transferRemoteInputFocus(visibleType);
610                 }
611                 NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType);
612                 if (visibleWrapper != null) {
613                     visibleWrapper.setContentHeight(mContentHeight, getMinContentHeightHint());
614                 }
615 
616                 if (animate && ((visibleType == VISIBLE_TYPE_EXPANDED && mExpandedChild != null)
617                         || (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null)
618                         || (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null)
619                         || visibleType == VISIBLE_TYPE_CONTRACTED)) {
620                     animateToVisibleType(visibleType);
621                 } else {
622                     updateViewVisibilities(visibleType);
623                 }
624                 mVisibleType = visibleType;
625                 if (changedType) {
626                     focusExpandButtonIfNecessary();
627                 }
628                 updateBackgroundColor(animate);
629             }
630         }
631     }
632 
forceUpdateVisibilities()633     private void forceUpdateVisibilities() {
634         boolean contractedVisible = mVisibleType == VISIBLE_TYPE_CONTRACTED
635                 || mTransformationStartVisibleType == VISIBLE_TYPE_CONTRACTED;
636         boolean expandedVisible = mVisibleType == VISIBLE_TYPE_EXPANDED
637                 || mTransformationStartVisibleType == VISIBLE_TYPE_EXPANDED;
638         boolean headsUpVisible = mVisibleType == VISIBLE_TYPE_HEADSUP
639                 || mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP;
640         boolean singleLineVisible = mVisibleType == VISIBLE_TYPE_SINGLELINE
641                 || mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE;
642         if (!contractedVisible) {
643             mContractedChild.setVisibility(View.INVISIBLE);
644         } else {
645             mContractedWrapper.setVisible(true);
646         }
647         if (mExpandedChild != null) {
648             if (!expandedVisible) {
649                 mExpandedChild.setVisibility(View.INVISIBLE);
650             } else {
651                 mExpandedWrapper.setVisible(true);
652             }
653         }
654         if (mHeadsUpChild != null) {
655             if (!headsUpVisible) {
656                 mHeadsUpChild.setVisibility(View.INVISIBLE);
657             } else {
658                 mHeadsUpWrapper.setVisible(true);
659             }
660         }
661         if (mSingleLineView != null) {
662             if (!singleLineVisible) {
663                 mSingleLineView.setVisibility(View.INVISIBLE);
664             } else {
665                 mSingleLineView.setVisible(true);
666             }
667         }
668     }
669 
updateBackgroundColor(boolean animate)670     public void updateBackgroundColor(boolean animate) {
671         int customBackgroundColor = getBackgroundColor(mVisibleType);
672         mContainingNotification.resetBackgroundAlpha();
673         mContainingNotification.setContentBackground(customBackgroundColor, animate, this);
674     }
675 
getVisibleType()676     public int getVisibleType() {
677         return mVisibleType;
678     }
679 
getBackgroundColorForExpansionState()680     public int getBackgroundColorForExpansionState() {
681         // When expanding or user locked we want the new type, when collapsing we want
682         // the original type
683         final int visibleType = (mContainingNotification.isGroupExpanded()
684                 || mContainingNotification.isUserLocked())
685                         ? calculateVisibleType()
686                         : getVisibleType();
687         return getBackgroundColor(visibleType);
688     }
689 
getBackgroundColor(int visibleType)690     public int getBackgroundColor(int visibleType) {
691         NotificationViewWrapper currentVisibleWrapper = getVisibleWrapper(visibleType);
692         int customBackgroundColor = 0;
693         if (currentVisibleWrapper != null) {
694             customBackgroundColor = currentVisibleWrapper.getCustomBackgroundColor();
695         }
696         return customBackgroundColor;
697     }
698 
updateViewVisibilities(int visibleType)699     private void updateViewVisibilities(int visibleType) {
700         boolean contractedVisible = visibleType == VISIBLE_TYPE_CONTRACTED;
701         mContractedWrapper.setVisible(contractedVisible);
702         if (mExpandedChild != null) {
703             boolean expandedVisible = visibleType == VISIBLE_TYPE_EXPANDED;
704             mExpandedWrapper.setVisible(expandedVisible);
705         }
706         if (mHeadsUpChild != null) {
707             boolean headsUpVisible = visibleType == VISIBLE_TYPE_HEADSUP;
708             mHeadsUpWrapper.setVisible(headsUpVisible);
709         }
710         if (mSingleLineView != null) {
711             boolean singleLineVisible = visibleType == VISIBLE_TYPE_SINGLELINE;
712             mSingleLineView.setVisible(singleLineVisible);
713         }
714     }
715 
animateToVisibleType(int visibleType)716     private void animateToVisibleType(int visibleType) {
717         final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
718         final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType);
719         if (shownView == hiddenView || hiddenView == null) {
720             shownView.setVisible(true);
721             return;
722         }
723         mAnimationStartVisibleType = mVisibleType;
724         shownView.transformFrom(hiddenView);
725         getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
726         hiddenView.transformTo(shownView, new Runnable() {
727             @Override
728             public void run() {
729                 if (hiddenView != getTransformableViewForVisibleType(mVisibleType)) {
730                     hiddenView.setVisible(false);
731                 }
732                 mAnimationStartVisibleType = UNDEFINED;
733             }
734         });
735     }
736 
transferRemoteInputFocus(int visibleType)737     private void transferRemoteInputFocus(int visibleType) {
738         if (visibleType == VISIBLE_TYPE_HEADSUP
739                 && mHeadsUpRemoteInput != null
740                 && (mExpandedRemoteInput != null && mExpandedRemoteInput.isActive())) {
741             mHeadsUpRemoteInput.stealFocusFrom(mExpandedRemoteInput);
742         }
743         if (visibleType == VISIBLE_TYPE_EXPANDED
744                 && mExpandedRemoteInput != null
745                 && (mHeadsUpRemoteInput != null && mHeadsUpRemoteInput.isActive())) {
746             mExpandedRemoteInput.stealFocusFrom(mHeadsUpRemoteInput);
747         }
748     }
749 
750     /**
751      * @param visibleType one of the static enum types in this view
752      * @return the corresponding transformable view according to the given visible type
753      */
getTransformableViewForVisibleType(int visibleType)754     private TransformableView getTransformableViewForVisibleType(int visibleType) {
755         switch (visibleType) {
756             case VISIBLE_TYPE_EXPANDED:
757                 return mExpandedWrapper;
758             case VISIBLE_TYPE_HEADSUP:
759                 return mHeadsUpWrapper;
760             case VISIBLE_TYPE_SINGLELINE:
761                 return mSingleLineView;
762             default:
763                 return mContractedWrapper;
764         }
765     }
766 
767     /**
768      * @param visibleType one of the static enum types in this view
769      * @return the corresponding view according to the given visible type
770      */
getViewForVisibleType(int visibleType)771     private View getViewForVisibleType(int visibleType) {
772         switch (visibleType) {
773             case VISIBLE_TYPE_EXPANDED:
774                 return mExpandedChild;
775             case VISIBLE_TYPE_HEADSUP:
776                 return mHeadsUpChild;
777             case VISIBLE_TYPE_SINGLELINE:
778                 return mSingleLineView;
779             default:
780                 return mContractedChild;
781         }
782     }
783 
getVisibleWrapper(int visibleType)784     private NotificationViewWrapper getVisibleWrapper(int visibleType) {
785         switch (visibleType) {
786             case VISIBLE_TYPE_EXPANDED:
787                 return mExpandedWrapper;
788             case VISIBLE_TYPE_HEADSUP:
789                 return mHeadsUpWrapper;
790             case VISIBLE_TYPE_CONTRACTED:
791                 return mContractedWrapper;
792             default:
793                 return null;
794         }
795     }
796 
797     /**
798      * @return one of the static enum types in this view, calculated form the current state
799      */
calculateVisibleType()800     public int calculateVisibleType() {
801         if (mUserExpanding) {
802             int height = !mIsChildInGroup || isGroupExpanded()
803                     || mContainingNotification.isExpanded(true /* allowOnKeyguard */)
804                     ? mContainingNotification.getMaxContentHeight()
805                     : mContainingNotification.getShowingLayout().getMinHeight();
806             if (height == 0) {
807                 height = mContentHeight;
808             }
809             int expandedVisualType = getVisualTypeForHeight(height);
810             int collapsedVisualType = mIsChildInGroup && !isGroupExpanded()
811                     ? VISIBLE_TYPE_SINGLELINE
812                     : getVisualTypeForHeight(mContainingNotification.getCollapsedHeight());
813             return mTransformationStartVisibleType == collapsedVisualType
814                     ? expandedVisualType
815                     : collapsedVisualType;
816         }
817         int intrinsicHeight = mContainingNotification.getIntrinsicHeight();
818         int viewHeight = mContentHeight;
819         if (intrinsicHeight != 0) {
820             // the intrinsicHeight might be 0 because it was just reset.
821             viewHeight = Math.min(mContentHeight, intrinsicHeight);
822         }
823         return getVisualTypeForHeight(viewHeight);
824     }
825 
getVisualTypeForHeight(float viewHeight)826     private int getVisualTypeForHeight(float viewHeight) {
827         boolean noExpandedChild = mExpandedChild == null;
828         if (!noExpandedChild && viewHeight == mExpandedChild.getHeight()) {
829             return VISIBLE_TYPE_EXPANDED;
830         }
831         if (!mUserExpanding && mIsChildInGroup && !isGroupExpanded()) {
832             return VISIBLE_TYPE_SINGLELINE;
833         }
834 
835         if ((mIsHeadsUp || mHeadsupDisappearRunning) && mHeadsUpChild != null) {
836             if (viewHeight <= mHeadsUpChild.getHeight() || noExpandedChild) {
837                 return VISIBLE_TYPE_HEADSUP;
838             } else {
839                 return VISIBLE_TYPE_EXPANDED;
840             }
841         } else {
842             if (noExpandedChild || (viewHeight <= mContractedChild.getHeight()
843                     && (!mIsChildInGroup || isGroupExpanded()
844                             || !mContainingNotification.isExpanded(true /* allowOnKeyguard */)))) {
845                 return VISIBLE_TYPE_CONTRACTED;
846             } else {
847                 return VISIBLE_TYPE_EXPANDED;
848             }
849         }
850     }
851 
isContentExpandable()852     public boolean isContentExpandable() {
853         return mExpandedChild != null;
854     }
855 
setDark(boolean dark, boolean fade, long delay)856     public void setDark(boolean dark, boolean fade, long delay) {
857         if (mContractedChild == null) {
858             return;
859         }
860         mDark = dark;
861         if (mVisibleType == VISIBLE_TYPE_CONTRACTED || !dark) {
862             mContractedWrapper.setDark(dark, fade, delay);
863         }
864         if (mVisibleType == VISIBLE_TYPE_EXPANDED || (mExpandedChild != null && !dark)) {
865             mExpandedWrapper.setDark(dark, fade, delay);
866         }
867         if (mVisibleType == VISIBLE_TYPE_HEADSUP || (mHeadsUpChild != null && !dark)) {
868             mHeadsUpWrapper.setDark(dark, fade, delay);
869         }
870         if (mSingleLineView != null && (mVisibleType == VISIBLE_TYPE_SINGLELINE || !dark)) {
871             mSingleLineView.setDark(dark, fade, delay);
872         }
873     }
874 
setHeadsUp(boolean headsUp)875     public void setHeadsUp(boolean headsUp) {
876         mIsHeadsUp = headsUp;
877         selectLayout(false /* animate */, true /* force */);
878         updateExpandButtons(mExpandable);
879     }
880 
881     @Override
hasOverlappingRendering()882     public boolean hasOverlappingRendering() {
883 
884         // This is not really true, but good enough when fading from the contracted to the expanded
885         // layout, and saves us some layers.
886         return false;
887     }
888 
setShowingLegacyBackground(boolean showing)889     public void setShowingLegacyBackground(boolean showing) {
890         mShowingLegacyBackground = showing;
891         updateShowingLegacyBackground();
892     }
893 
updateShowingLegacyBackground()894     private void updateShowingLegacyBackground() {
895         if (mContractedChild != null) {
896             mContractedWrapper.setShowingLegacyBackground(mShowingLegacyBackground);
897         }
898         if (mExpandedChild != null) {
899             mExpandedWrapper.setShowingLegacyBackground(mShowingLegacyBackground);
900         }
901         if (mHeadsUpChild != null) {
902             mHeadsUpWrapper.setShowingLegacyBackground(mShowingLegacyBackground);
903         }
904     }
905 
setIsChildInGroup(boolean isChildInGroup)906     public void setIsChildInGroup(boolean isChildInGroup) {
907         mIsChildInGroup = isChildInGroup;
908         updateSingleLineView();
909     }
910 
onNotificationUpdated(NotificationData.Entry entry)911     public void onNotificationUpdated(NotificationData.Entry entry) {
912         mStatusBarNotification = entry.notification;
913         mBeforeN = entry.targetSdk < Build.VERSION_CODES.N;
914         updateSingleLineView();
915         applyRemoteInput(entry);
916         if (mContractedChild != null) {
917             mContractedWrapper.notifyContentUpdated(entry.notification);
918         }
919         if (mExpandedChild != null) {
920             mExpandedWrapper.notifyContentUpdated(entry.notification);
921         }
922         if (mHeadsUpChild != null) {
923             mHeadsUpWrapper.notifyContentUpdated(entry.notification);
924         }
925         updateShowingLegacyBackground();
926         mForceSelectNextLayout = true;
927         setDark(mDark, false /* animate */, 0 /* delay */);
928         mPreviousExpandedRemoteInputIntent = null;
929         mPreviousHeadsUpRemoteInputIntent = null;
930     }
931 
932     private void updateSingleLineView() {
933         if (mIsChildInGroup) {
934             mSingleLineView = mHybridGroupManager.bindFromNotification(
935                     mSingleLineView, mStatusBarNotification.getNotification());
936         } else if (mSingleLineView != null) {
937             removeView(mSingleLineView);
938             mSingleLineView = null;
939         }
940     }
941 
942     private void applyRemoteInput(final NotificationData.Entry entry) {
943         if (mRemoteInputController == null) {
944             return;
945         }
946 
947         boolean hasRemoteInput = false;
948 
949         Notification.Action[] actions = entry.notification.getNotification().actions;
950         if (actions != null) {
951             for (Notification.Action a : actions) {
952                 if (a.getRemoteInputs() != null) {
953                     for (RemoteInput ri : a.getRemoteInputs()) {
954                         if (ri.getAllowFreeFormInput()) {
955                             hasRemoteInput = true;
956                             break;
957                         }
958                     }
959                 }
960             }
961         }
962 
963         View bigContentView = mExpandedChild;
964         if (bigContentView != null) {
965             mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasRemoteInput,
966                     mPreviousExpandedRemoteInputIntent);
967         } else {
968             mExpandedRemoteInput = null;
969         }
970 
971         View headsUpContentView = mHeadsUpChild;
972         if (headsUpContentView != null) {
973             mHeadsUpRemoteInput = applyRemoteInput(headsUpContentView, entry, hasRemoteInput,
974                     mPreviousHeadsUpRemoteInputIntent);
975         } else {
976             mHeadsUpRemoteInput = null;
977         }
978     }
979 
980     private RemoteInputView applyRemoteInput(View view, NotificationData.Entry entry,
981             boolean hasRemoteInput, PendingIntent existingPendingIntent) {
982         View actionContainerCandidate = view.findViewById(
983                 com.android.internal.R.id.actions_container);
984         if (actionContainerCandidate instanceof FrameLayout) {
985             RemoteInputView existing = (RemoteInputView)
986                     view.findViewWithTag(RemoteInputView.VIEW_TAG);
987 
988             if (existing != null) {
989                 existing.onNotificationUpdateOrReset();
990             }
991 
992             if (existing == null && hasRemoteInput) {
993                 ViewGroup actionContainer = (FrameLayout) actionContainerCandidate;
994                 RemoteInputView riv = RemoteInputView.inflate(
995                         mContext, actionContainer, entry, mRemoteInputController);
996 
997                 riv.setVisibility(View.INVISIBLE);
998                 actionContainer.addView(riv, new LayoutParams(
999                         ViewGroup.LayoutParams.MATCH_PARENT,
1000                         ViewGroup.LayoutParams.MATCH_PARENT)
1001                 );
1002                 existing = riv;
1003             }
1004             if (hasRemoteInput) {
1005                 int color = entry.notification.getNotification().color;
1006                 if (color == Notification.COLOR_DEFAULT) {
1007                     color = mContext.getColor(R.color.default_remote_input_background);
1008                 }
1009                 existing.setBackgroundColor(NotificationColorUtil.ensureTextBackgroundColor(color,
1010                         mContext.getColor(R.color.remote_input_text_enabled),
1011                         mContext.getColor(R.color.remote_input_hint)));
1012 
1013                 if (existingPendingIntent != null || existing.isActive()) {
1014                     // The current action could be gone, or the pending intent no longer valid.
1015                     // If we find a matching action in the new notification, focus, otherwise close.
1016                     Notification.Action[] actions = entry.notification.getNotification().actions;
1017                     if (existingPendingIntent != null) {
1018                         existing.setPendingIntent(existingPendingIntent);
1019                     }
1020                     if (existing.updatePendingIntentFromActions(actions)) {
1021                         if (!existing.isActive()) {
1022                             existing.focus();
1023                         }
1024                     } else {
1025                         if (existing.isActive()) {
1026                             existing.close();
1027                         }
1028                     }
1029                 }
1030             }
1031             return existing;
1032         }
1033         return null;
1034     }
1035 
1036     public void closeRemoteInput() {
1037         if (mHeadsUpRemoteInput != null) {
1038             mHeadsUpRemoteInput.close();
1039         }
1040         if (mExpandedRemoteInput != null) {
1041             mExpandedRemoteInput.close();
1042         }
1043     }
1044 
1045     public void setGroupManager(NotificationGroupManager groupManager) {
1046         mGroupManager = groupManager;
1047     }
1048 
1049     public void setRemoteInputController(RemoteInputController r) {
1050         mRemoteInputController = r;
1051     }
1052 
1053     public void setExpandClickListener(OnClickListener expandClickListener) {
1054         mExpandClickListener = expandClickListener;
1055     }
1056 
1057     public void updateExpandButtons(boolean expandable) {
1058         mExpandable = expandable;
1059         // if the expanded child has the same height as the collapsed one we hide it.
1060         if (mExpandedChild != null && mExpandedChild.getHeight() != 0) {
1061             if ((!mIsHeadsUp || mHeadsUpChild == null)) {
1062                 if (mExpandedChild.getHeight() == mContractedChild.getHeight()) {
1063                     expandable = false;
1064                 }
1065             } else if (mExpandedChild.getHeight() == mHeadsUpChild.getHeight()) {
1066                 expandable = false;
1067             }
1068         }
1069         if (mExpandedChild != null) {
1070             mExpandedWrapper.updateExpandability(expandable, mExpandClickListener);
1071         }
1072         if (mContractedChild != null) {
1073             mContractedWrapper.updateExpandability(expandable, mExpandClickListener);
1074         }
1075         if (mHeadsUpChild != null) {
1076             mHeadsUpWrapper.updateExpandability(expandable,  mExpandClickListener);
1077         }
1078     }
1079 
1080     public NotificationHeaderView getNotificationHeader() {
1081         NotificationHeaderView header = null;
1082         if (mContractedChild != null) {
1083             header = mContractedWrapper.getNotificationHeader();
1084         }
1085         if (header == null && mExpandedChild != null) {
1086             header = mExpandedWrapper.getNotificationHeader();
1087         }
1088         if (header == null && mHeadsUpChild != null) {
1089             header = mHeadsUpWrapper.getNotificationHeader();
1090         }
1091         return header;
1092     }
1093 
1094     public NotificationHeaderView getVisibleNotificationHeader() {
1095         NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
1096         return wrapper == null ? null : wrapper.getNotificationHeader();
1097     }
1098 
1099     public void setContainingNotification(ExpandableNotificationRow containingNotification) {
1100         mContainingNotification = containingNotification;
1101     }
1102 
1103     public void requestSelectLayout(boolean needsAnimation) {
1104         selectLayout(needsAnimation, false);
1105     }
1106 
1107     public void reInflateViews() {
1108         if (mIsChildInGroup && mSingleLineView != null) {
1109             removeView(mSingleLineView);
1110             mSingleLineView = null;
1111             updateSingleLineView();
1112         }
1113     }
1114 
1115     public void setUserExpanding(boolean userExpanding) {
1116         mUserExpanding = userExpanding;
1117         if (userExpanding) {
1118             mTransformationStartVisibleType = mVisibleType;
1119         } else {
1120             mTransformationStartVisibleType = UNDEFINED;
1121             mVisibleType = calculateVisibleType();
1122             updateViewVisibilities(mVisibleType);
1123             updateBackgroundColor(false);
1124         }
1125     }
1126 
1127     /**
1128      * Set by how much the single line view should be indented. Used when a overflow indicator is
1129      * present and only during measuring
1130      */
1131     public void setSingleLineWidthIndention(int singleLineWidthIndention) {
1132         if (singleLineWidthIndention != mSingleLineWidthIndention) {
1133             mSingleLineWidthIndention = singleLineWidthIndention;
1134             mContainingNotification.forceLayout();
1135             forceLayout();
1136         }
1137     }
1138 
1139     public HybridNotificationView getSingleLineView() {
1140         return mSingleLineView;
1141     }
1142 
1143     public void setRemoved() {
1144         if (mExpandedRemoteInput != null) {
1145             mExpandedRemoteInput.setRemoved();
1146         }
1147         if (mHeadsUpRemoteInput != null) {
1148             mHeadsUpRemoteInput.setRemoved();
1149         }
1150     }
1151 
1152     public void setContentHeightAnimating(boolean animating) {
1153         if (!animating) {
1154             mContentHeightAtAnimationStart = UNDEFINED;
1155         }
1156     }
1157 
1158     public void setHeadsupDisappearRunning(boolean headsupDisappearRunning) {
1159         mHeadsupDisappearRunning = headsupDisappearRunning;
1160         selectLayout(false /* animate */, true /* force */);
1161     }
1162 
1163     public void setFocusOnVisibilityChange() {
1164         mFocusOnVisibilityChange = true;
1165     }
1166 }
1167