1 /*
2  * Copyright (C) 2013 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.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.graphics.drawable.AnimatedVectorDrawable;
24 import android.graphics.drawable.AnimationDrawable;
25 import android.graphics.drawable.ColorDrawable;
26 import android.graphics.drawable.Drawable;
27 import android.service.notification.StatusBarNotification;
28 import android.util.AttributeSet;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.ViewStub;
32 import android.view.accessibility.AccessibilityEvent;
33 import android.view.animation.LinearInterpolator;
34 import android.widget.ImageView;
35 
36 import com.android.systemui.R;
37 import com.android.systemui.statusbar.phone.NotificationGroupManager;
38 import com.android.systemui.statusbar.phone.PhoneStatusBar;
39 import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
40 import com.android.systemui.statusbar.stack.StackScrollState;
41 import com.android.systemui.statusbar.stack.StackStateAnimator;
42 import com.android.systemui.statusbar.stack.StackViewState;
43 
44 import java.util.List;
45 
46 public class ExpandableNotificationRow extends ActivatableNotificationView {
47 
48     private static final int DEFAULT_DIVIDER_ALPHA = 0x29;
49     private static final int COLORED_DIVIDER_ALPHA = 0x7B;
50     private final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
51     private int mRowMinHeight;
52 
53     /** Does this row contain layouts that can adapt to row expansion */
54     private boolean mExpandable;
55     /** Has the user actively changed the expansion state of this row */
56     private boolean mHasUserChangedExpansion;
57     /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
58     private boolean mUserExpanded;
59     /** Is the user touching this row */
60     private boolean mUserLocked;
61     /** Are we showing the "public" version */
62     private boolean mShowingPublic;
63     private boolean mSensitive;
64     private boolean mShowingPublicInitialized;
65     private boolean mHideSensitiveForIntrinsicHeight;
66 
67     /**
68      * Is this notification expanded by the system. The expansion state can be overridden by the
69      * user expansion.
70      */
71     private boolean mIsSystemExpanded;
72 
73     /**
74      * Whether the notification expansion is disabled. This is the case on Keyguard.
75      */
76     private boolean mExpansionDisabled;
77 
78     private NotificationContentView mPublicLayout;
79     private NotificationContentView mPrivateLayout;
80     private int mMaxExpandHeight;
81     private int mHeadsUpHeight;
82     private View mVetoButton;
83     private boolean mClearable;
84     private ExpansionLogger mLogger;
85     private String mLoggingKey;
86     private boolean mWasReset;
87     private NotificationGuts mGuts;
88     private StatusBarNotification mStatusBarNotification;
89     private boolean mIsHeadsUp;
90     private View mExpandButton;
91     private View mExpandButtonDivider;
92     private ViewStub mExpandButtonStub;
93     private ViewStub mChildrenContainerStub;
94     private NotificationGroupManager mGroupManager;
95     private View mExpandButtonContainer;
96     private boolean mChildrenExpanded;
97     private NotificationChildrenContainer mChildrenContainer;
98     private ValueAnimator mChildExpandAnimator;
99     private float mChildrenExpandProgress;
100     private float mExpandButtonStart;
101     private ViewStub mGutsStub;
102     private boolean mHasExpandAction;
103     private boolean mIsSystemChildExpanded;
104     private boolean mIsPinned;
105     private OnClickListener mExpandClickListener = new OnClickListener() {
106         @Override
107         public void onClick(View v) {
108             mGroupManager.setGroupExpanded(mStatusBarNotification,
109                     !mChildrenExpanded);
110         }
111     };
112 
getPrivateLayout()113     public NotificationContentView getPrivateLayout() {
114         return mPrivateLayout;
115     }
116 
getPublicLayout()117     public NotificationContentView getPublicLayout() {
118         return mPublicLayout;
119     }
120 
setIconAnimationRunning(boolean running)121     public void setIconAnimationRunning(boolean running) {
122         setIconAnimationRunning(running, mPublicLayout);
123         setIconAnimationRunning(running, mPrivateLayout);
124     }
125 
setIconAnimationRunning(boolean running, NotificationContentView layout)126     private void setIconAnimationRunning(boolean running, NotificationContentView layout) {
127         if (layout != null) {
128             View contractedChild = layout.getContractedChild();
129             View expandedChild = layout.getExpandedChild();
130             View headsUpChild = layout.getHeadsUpChild();
131             setIconAnimationRunningForChild(running, contractedChild);
132             setIconAnimationRunningForChild(running, expandedChild);
133             setIconAnimationRunningForChild(running, headsUpChild);
134         }
135     }
136 
setIconAnimationRunningForChild(boolean running, View child)137     private void setIconAnimationRunningForChild(boolean running, View child) {
138         if (child != null) {
139             ImageView icon = (ImageView) child.findViewById(com.android.internal.R.id.icon);
140             setIconRunning(icon, running);
141             ImageView rightIcon = (ImageView) child.findViewById(
142                     com.android.internal.R.id.right_icon);
143             setIconRunning(rightIcon, running);
144         }
145     }
146 
setIconRunning(ImageView imageView, boolean running)147     private void setIconRunning(ImageView imageView, boolean running) {
148         if (imageView != null) {
149             Drawable drawable = imageView.getDrawable();
150             if (drawable instanceof AnimationDrawable) {
151                 AnimationDrawable animationDrawable = (AnimationDrawable) drawable;
152                 if (running) {
153                     animationDrawable.start();
154                 } else {
155                     animationDrawable.stop();
156                 }
157             } else if (drawable instanceof AnimatedVectorDrawable) {
158                 AnimatedVectorDrawable animationDrawable = (AnimatedVectorDrawable) drawable;
159                 if (running) {
160                     animationDrawable.start();
161                 } else {
162                     animationDrawable.stop();
163                 }
164             }
165         }
166     }
167 
setStatusBarNotification(StatusBarNotification statusBarNotification)168     public void setStatusBarNotification(StatusBarNotification statusBarNotification) {
169         mStatusBarNotification = statusBarNotification;
170         updateVetoButton();
171         updateExpandButton();
172     }
173 
getStatusBarNotification()174     public StatusBarNotification getStatusBarNotification() {
175         return mStatusBarNotification;
176     }
177 
isHeadsUp()178     public boolean isHeadsUp() {
179         return mIsHeadsUp;
180     }
181 
setHeadsUp(boolean isHeadsUp)182     public void setHeadsUp(boolean isHeadsUp) {
183         int intrinsicBefore = getIntrinsicHeight();
184         mIsHeadsUp = isHeadsUp;
185         mPrivateLayout.setHeadsUp(isHeadsUp);
186         if (intrinsicBefore != getIntrinsicHeight()) {
187             notifyHeightChanged(false  /* needsAnimation */);
188         }
189     }
190 
setGroupManager(NotificationGroupManager groupManager)191     public void setGroupManager(NotificationGroupManager groupManager) {
192         mGroupManager = groupManager;
193     }
194 
addChildNotification(ExpandableNotificationRow row)195     public void addChildNotification(ExpandableNotificationRow row) {
196         addChildNotification(row, -1);
197     }
198 
199     /**
200      * Add a child notification to this view.
201      *
202      * @param row the row to add
203      * @param childIndex the index to add it at, if -1 it will be added at the end
204      */
addChildNotification(ExpandableNotificationRow row, int childIndex)205     public void addChildNotification(ExpandableNotificationRow row, int childIndex) {
206         if (mChildrenContainer == null) {
207             mChildrenContainerStub.inflate();
208         }
209         mChildrenContainer.addNotification(row, childIndex);
210     }
211 
removeChildNotification(ExpandableNotificationRow row)212     public void removeChildNotification(ExpandableNotificationRow row) {
213         if (mChildrenContainer != null) {
214             mChildrenContainer.removeNotification(row);
215         }
216     }
217 
218     @Override
areChildrenExpanded()219     public boolean areChildrenExpanded() {
220         return mChildrenExpanded;
221     }
222 
getNotificationChildren()223     public List<ExpandableNotificationRow> getNotificationChildren() {
224         return mChildrenContainer == null ? null : mChildrenContainer.getNotificationChildren();
225     }
226 
227     /**
228      * Apply the order given in the list to the children.
229      *
230      * @param childOrder the new list order
231      * @return whether the list order has changed
232      */
applyChildOrder(List<ExpandableNotificationRow> childOrder)233     public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
234         return mChildrenContainer != null && mChildrenContainer.applyChildOrder(childOrder);
235     }
236 
getChildrenStates(StackScrollState resultState)237     public void getChildrenStates(StackScrollState resultState) {
238         if (mChildrenExpanded) {
239             StackViewState parentState = resultState.getViewStateForView(this);
240             mChildrenContainer.getState(resultState, parentState);
241         }
242     }
243 
applyChildrenState(StackScrollState state)244     public void applyChildrenState(StackScrollState state) {
245         if (mChildrenExpanded) {
246             mChildrenContainer.applyState(state);
247         }
248     }
249 
prepareExpansionChanged(StackScrollState state)250     public void prepareExpansionChanged(StackScrollState state) {
251         if (mChildrenExpanded) {
252             mChildrenContainer.prepareExpansionChanged(state);
253         }
254     }
255 
startChildAnimation(StackScrollState finalState, StackStateAnimator stateAnimator, boolean withDelays, long delay, long duration)256     public void startChildAnimation(StackScrollState finalState,
257             StackStateAnimator stateAnimator, boolean withDelays, long delay, long duration) {
258         if (mChildrenExpanded) {
259             mChildrenContainer.startAnimationToState(finalState, stateAnimator, withDelays, delay,
260                     duration);
261         }
262     }
263 
getViewAtPosition(float y)264     public ExpandableNotificationRow getViewAtPosition(float y) {
265         if (!mChildrenExpanded) {
266             return this;
267         } else {
268             ExpandableNotificationRow view = mChildrenContainer.getViewAtPosition(y);
269             return view == null ? this : view;
270         }
271     }
272 
getGuts()273     public NotificationGuts getGuts() {
274         return mGuts;
275     }
276 
calculateContentHeightFromActualHeight(int actualHeight)277     protected int calculateContentHeightFromActualHeight(int actualHeight) {
278         int realActualHeight = actualHeight;
279         if (hasBottomDecor()) {
280             realActualHeight -= getBottomDecorHeight();
281         }
282         realActualHeight = Math.max(getMinHeight(), realActualHeight);
283         return realActualHeight;
284     }
285 
286     /**
287      * Set this notification to be pinned to the top if {@link #isHeadsUp()} is true. By doing this
288      * the notification will be rendered on top of the screen.
289      *
290      * @param pinned whether it is pinned
291      */
setPinned(boolean pinned)292     public void setPinned(boolean pinned) {
293         mIsPinned = pinned;
294     }
295 
isPinned()296     public boolean isPinned() {
297         return mIsPinned;
298     }
299 
getHeadsUpHeight()300     public int getHeadsUpHeight() {
301         return mHeadsUpHeight;
302     }
303 
304     public interface ExpansionLogger {
logNotificationExpansion(String key, boolean userAction, boolean expanded)305         public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
306     }
307 
ExpandableNotificationRow(Context context, AttributeSet attrs)308     public ExpandableNotificationRow(Context context, AttributeSet attrs) {
309         super(context, attrs);
310     }
311 
312     /**
313      * Resets this view so it can be re-used for an updated notification.
314      */
315     @Override
reset()316     public void reset() {
317         super.reset();
318         mRowMinHeight = 0;
319         final boolean wasExpanded = isExpanded();
320         mMaxViewHeight = 0;
321         mExpandable = false;
322         mHasUserChangedExpansion = false;
323         mUserLocked = false;
324         mShowingPublic = false;
325         mSensitive = false;
326         mShowingPublicInitialized = false;
327         mIsSystemExpanded = false;
328         mExpansionDisabled = false;
329         mPublicLayout.reset(mIsHeadsUp);
330         mPrivateLayout.reset(mIsHeadsUp);
331         resetHeight();
332         logExpansionEvent(false, wasExpanded);
333     }
334 
resetHeight()335     public void resetHeight() {
336         if (mIsHeadsUp) {
337             resetActualHeight();
338         }
339         mMaxExpandHeight = 0;
340         mHeadsUpHeight = 0;
341         mWasReset = true;
342         onHeightReset();
343         requestLayout();
344     }
345 
346     @Override
filterMotionEvent(MotionEvent event)347     protected boolean filterMotionEvent(MotionEvent event) {
348         return mIsHeadsUp || super.filterMotionEvent(event);
349     }
350 
351     @Override
onFinishInflate()352     protected void onFinishInflate() {
353         super.onFinishInflate();
354         mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic);
355         mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
356         mGutsStub = (ViewStub) findViewById(R.id.notification_guts_stub);
357         mGutsStub.setOnInflateListener(new ViewStub.OnInflateListener() {
358             @Override
359             public void onInflate(ViewStub stub, View inflated) {
360                 mGuts = (NotificationGuts) inflated;
361                 mGuts.setClipTopAmount(getClipTopAmount());
362                 mGuts.setActualHeight(getActualHeight());
363                 mGutsStub = null;
364             }
365         });
366         mExpandButtonStub = (ViewStub) findViewById(R.id.more_button_stub);
367         mExpandButtonStub.setOnInflateListener(new ViewStub.OnInflateListener() {
368 
369             @Override
370             public void onInflate(ViewStub stub, View inflated) {
371                 mExpandButtonContainer = inflated;
372                 mExpandButton = inflated.findViewById(R.id.notification_expand_button);
373                 mExpandButtonDivider = inflated.findViewById(R.id.notification_expand_divider);
374                 mExpandButtonContainer.setOnClickListener(mExpandClickListener);
375             }
376         });
377         mChildrenContainerStub = (ViewStub) findViewById(R.id.child_container_stub);
378         mChildrenContainerStub.setOnInflateListener(new ViewStub.OnInflateListener() {
379 
380             @Override
381             public void onInflate(ViewStub stub, View inflated) {
382                 mChildrenContainer = (NotificationChildrenContainer) inflated;
383                 mChildrenContainer.setCollapseClickListener(mExpandClickListener);
384                 updateChildrenVisibility(false);
385             }
386         });
387         mVetoButton = findViewById(R.id.veto);
388     }
389 
inflateGuts()390     public void inflateGuts() {
391         if (mGuts == null) {
392             mGutsStub.inflate();
393         }
394     }
395 
updateChildrenVisibility(boolean animated)396     private void updateChildrenVisibility(boolean animated) {
397         if (mChildrenContainer == null) {
398             return;
399         }
400         if (mChildExpandAnimator != null) {
401             mChildExpandAnimator.cancel();
402         }
403         float targetProgress = mChildrenExpanded ? 1.0f : 0.0f;
404         if (animated) {
405             if (mChildrenExpanded) {
406                 mChildrenContainer.setVisibility(VISIBLE);
407             }
408             mExpandButtonStart = mExpandButtonContainer.getTranslationY();
409             mChildExpandAnimator = ValueAnimator.ofFloat(mChildrenExpandProgress, targetProgress);
410             mChildExpandAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
411                 @Override
412                 public void onAnimationUpdate(ValueAnimator animation) {
413                     setChildrenExpandProgress((float) animation.getAnimatedValue());
414                 }
415             });
416             mChildExpandAnimator.addListener(new AnimatorListenerAdapter() {
417                 @Override
418                 public void onAnimationEnd(Animator animation) {
419                     mChildExpandAnimator = null;
420                     if (!mChildrenExpanded) {
421                         mChildrenContainer.setVisibility(INVISIBLE);
422                     }
423                 }
424             });
425             mChildExpandAnimator.setInterpolator(mLinearInterpolator);
426             mChildExpandAnimator.setDuration(
427                     StackStateAnimator.ANIMATION_DURATION_EXPAND_CLICKED);
428             mChildExpandAnimator.start();
429         } else {
430             setChildrenExpandProgress(targetProgress);
431             mChildrenContainer.setVisibility(mChildrenExpanded ? VISIBLE : INVISIBLE);
432         }
433     }
434 
setChildrenExpandProgress(float progress)435     private void setChildrenExpandProgress(float progress) {
436         mChildrenExpandProgress = progress;
437         updateExpandButtonAppearance();
438         NotificationContentView showingLayout = getShowingLayout();
439         float alpha = 1.0f - mChildrenExpandProgress;
440         alpha = PhoneStatusBar.ALPHA_OUT.getInterpolation(alpha);
441         showingLayout.setAlpha(alpha);
442     }
443 
444     @Override
onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event)445     public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
446         if (super.onRequestSendAccessibilityEventInternal(child, event)) {
447             // Add a record for the entire layout since its content is somehow small.
448             // The event comes from a leaf view that is interacted with.
449             AccessibilityEvent record = AccessibilityEvent.obtain();
450             onInitializeAccessibilityEvent(record);
451             dispatchPopulateAccessibilityEvent(record);
452             event.appendRecord(record);
453             return true;
454         }
455         return false;
456     }
457 
458     @Override
setDark(boolean dark, boolean fade, long delay)459     public void setDark(boolean dark, boolean fade, long delay) {
460         super.setDark(dark, fade, delay);
461         final NotificationContentView showing = getShowingLayout();
462         if (showing != null) {
463             showing.setDark(dark, fade, delay);
464         }
465     }
466 
setHeightRange(int rowMinHeight, int rowMaxHeight)467     public void setHeightRange(int rowMinHeight, int rowMaxHeight) {
468         mRowMinHeight = rowMinHeight;
469         mMaxViewHeight = rowMaxHeight;
470     }
471 
isExpandable()472     public boolean isExpandable() {
473         return mExpandable;
474     }
475 
setExpandable(boolean expandable)476     public void setExpandable(boolean expandable) {
477         mExpandable = expandable;
478     }
479 
480     /**
481      * @return whether the user has changed the expansion state
482      */
hasUserChangedExpansion()483     public boolean hasUserChangedExpansion() {
484         return mHasUserChangedExpansion;
485     }
486 
isUserExpanded()487     public boolean isUserExpanded() {
488         return mUserExpanded;
489     }
490 
491     /**
492      * Set this notification to be expanded by the user
493      *
494      * @param userExpanded whether the user wants this notification to be expanded
495      */
setUserExpanded(boolean userExpanded)496     public void setUserExpanded(boolean userExpanded) {
497         if (userExpanded && !mExpandable) return;
498         final boolean wasExpanded = isExpanded();
499         mHasUserChangedExpansion = true;
500         mUserExpanded = userExpanded;
501         logExpansionEvent(true, wasExpanded);
502     }
503 
resetUserExpansion()504     public void resetUserExpansion() {
505         mHasUserChangedExpansion = false;
506         mUserExpanded = false;
507     }
508 
isUserLocked()509     public boolean isUserLocked() {
510         return mUserLocked;
511     }
512 
setUserLocked(boolean userLocked)513     public void setUserLocked(boolean userLocked) {
514         mUserLocked = userLocked;
515     }
516 
517     /**
518      * @return has the system set this notification to be expanded
519      */
isSystemExpanded()520     public boolean isSystemExpanded() {
521         return mIsSystemExpanded;
522     }
523 
524     /**
525      * Set this notification to be expanded by the system.
526      *
527      * @param expand whether the system wants this notification to be expanded.
528      */
setSystemExpanded(boolean expand)529     public void setSystemExpanded(boolean expand) {
530         if (expand != mIsSystemExpanded) {
531             final boolean wasExpanded = isExpanded();
532             mIsSystemExpanded = expand;
533             notifyHeightChanged(false /* needsAnimation */);
534             logExpansionEvent(false, wasExpanded);
535         }
536     }
537 
538     /**
539      * @param expansionDisabled whether to prevent notification expansion
540      */
setExpansionDisabled(boolean expansionDisabled)541     public void setExpansionDisabled(boolean expansionDisabled) {
542         if (expansionDisabled != mExpansionDisabled) {
543             final boolean wasExpanded = isExpanded();
544             mExpansionDisabled = expansionDisabled;
545             logExpansionEvent(false, wasExpanded);
546             if (wasExpanded != isExpanded()) {
547                 notifyHeightChanged(false  /* needsAnimation */);
548             }
549         }
550     }
551 
552     /**
553      * @return Can the underlying notification be cleared?
554      */
isClearable()555     public boolean isClearable() {
556         return mStatusBarNotification != null && mStatusBarNotification.isClearable();
557     }
558 
559     /**
560      * Apply an expansion state to the layout.
561      */
applyExpansionToLayout()562     public void applyExpansionToLayout() {
563         boolean expand = isExpanded();
564         if (expand && mExpandable) {
565             setContentHeight(mMaxExpandHeight);
566         } else {
567             setContentHeight(mRowMinHeight);
568         }
569     }
570 
571     @Override
getIntrinsicHeight()572     public int getIntrinsicHeight() {
573         if (isUserLocked()) {
574             return getActualHeight();
575         }
576         boolean inExpansionState = isExpanded();
577         int maxContentHeight;
578         if (mSensitive && mHideSensitiveForIntrinsicHeight) {
579             return mRowMinHeight;
580         } else if (mIsHeadsUp) {
581             if (inExpansionState) {
582                 maxContentHeight = Math.max(mMaxExpandHeight, mHeadsUpHeight);
583             } else {
584                 maxContentHeight = Math.max(mRowMinHeight, mHeadsUpHeight);
585             }
586         } else if ((!inExpansionState && !mChildrenExpanded)) {
587             maxContentHeight = mRowMinHeight;
588         } else if (mChildrenExpanded) {
589             maxContentHeight = mChildrenContainer.getIntrinsicHeight();
590         } else {
591             maxContentHeight = getMaxExpandHeight();
592         }
593         return maxContentHeight + getBottomDecorHeight();
594     }
595 
596     @Override
hasBottomDecor()597     protected boolean hasBottomDecor() {
598         return BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS
599                 && !mIsHeadsUp && mGroupManager.hasGroupChildren(mStatusBarNotification);
600     }
601 
602     @Override
canHaveBottomDecor()603     protected boolean canHaveBottomDecor() {
604         return BaseStatusBar.ENABLE_CHILD_NOTIFICATIONS && !mIsHeadsUp;
605     }
606 
607     /**
608      * Check whether the view state is currently expanded. This is given by the system in {@link
609      * #setSystemExpanded(boolean)} and can be overridden by user expansion or
610      * collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this
611      * view can differ from this state, if layout params are modified from outside.
612      *
613      * @return whether the view state is currently expanded.
614      */
isExpanded()615     private boolean isExpanded() {
616         return !mExpansionDisabled
617                 && (!hasUserChangedExpansion() && (isSystemExpanded() || isSystemChildExpanded())
618                 || isUserExpanded());
619     }
620 
isSystemChildExpanded()621     private boolean isSystemChildExpanded() {
622         return mIsSystemChildExpanded;
623     }
624 
setSystemChildExpanded(boolean expanded)625     public void setSystemChildExpanded(boolean expanded) {
626         mIsSystemChildExpanded = expanded;
627     }
628 
629     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)630     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
631         super.onLayout(changed, left, top, right, bottom);
632         boolean updateExpandHeight = mMaxExpandHeight == 0 && !mWasReset;
633         updateMaxHeights();
634         if (updateExpandHeight) {
635             applyExpansionToLayout();
636         }
637         mWasReset = false;
638     }
639 
640     @Override
isChildInvisible(View child)641     protected boolean isChildInvisible(View child) {
642 
643         // We don't want to layout the ChildrenContainer if this is a heads-up view, otherwise the
644         // view will get too high and the shadows will be off.
645         boolean isInvisibleChildContainer = child == mChildrenContainer && mIsHeadsUp;
646         return super.isChildInvisible(child) || isInvisibleChildContainer;
647     }
648 
updateMaxHeights()649     private void updateMaxHeights() {
650         int intrinsicBefore = getIntrinsicHeight();
651         View expandedChild = mPrivateLayout.getExpandedChild();
652         if (expandedChild == null) {
653             expandedChild = mPrivateLayout.getContractedChild();
654         }
655         mMaxExpandHeight = expandedChild.getHeight();
656         View headsUpChild = mPrivateLayout.getHeadsUpChild();
657         if (headsUpChild == null) {
658             headsUpChild = mPrivateLayout.getContractedChild();
659         }
660         mHeadsUpHeight = headsUpChild.getHeight();
661         if (intrinsicBefore != getIntrinsicHeight()) {
662             notifyHeightChanged(false  /* needsAnimation */);
663         }
664     }
665 
setSensitive(boolean sensitive)666     public void setSensitive(boolean sensitive) {
667         mSensitive = sensitive;
668     }
669 
setHideSensitiveForIntrinsicHeight(boolean hideSensitive)670     public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) {
671         mHideSensitiveForIntrinsicHeight = hideSensitive;
672     }
673 
setHideSensitive(boolean hideSensitive, boolean animated, long delay, long duration)674     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
675             long duration) {
676         boolean oldShowingPublic = mShowingPublic;
677         mShowingPublic = mSensitive && hideSensitive;
678         if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) {
679             return;
680         }
681 
682         // bail out if no public version
683         if (mPublicLayout.getChildCount() == 0) return;
684 
685         if (!animated) {
686             mPublicLayout.animate().cancel();
687             mPrivateLayout.animate().cancel();
688             mPublicLayout.setAlpha(1f);
689             mPrivateLayout.setAlpha(1f);
690             mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE);
691             mPrivateLayout.setVisibility(mShowingPublic ? View.INVISIBLE : View.VISIBLE);
692         } else {
693             animateShowingPublic(delay, duration);
694         }
695 
696         updateVetoButton();
697         mShowingPublicInitialized = true;
698     }
699 
animateShowingPublic(long delay, long duration)700     private void animateShowingPublic(long delay, long duration) {
701         final View source = mShowingPublic ? mPrivateLayout : mPublicLayout;
702         View target = mShowingPublic ? mPublicLayout : mPrivateLayout;
703         source.setVisibility(View.VISIBLE);
704         target.setVisibility(View.VISIBLE);
705         target.setAlpha(0f);
706         source.animate().cancel();
707         target.animate().cancel();
708         source.animate()
709                 .alpha(0f)
710                 .setStartDelay(delay)
711                 .setDuration(duration)
712                 .withEndAction(new Runnable() {
713                     @Override
714                     public void run() {
715                         source.setVisibility(View.INVISIBLE);
716                     }
717                 });
718         target.animate()
719                 .alpha(1f)
720                 .setStartDelay(delay)
721                 .setDuration(duration);
722     }
723 
updateVetoButton()724     private void updateVetoButton() {
725         // public versions cannot be dismissed
726         mVetoButton.setVisibility(isClearable() && !mShowingPublic ? View.VISIBLE : View.GONE);
727     }
728 
setChildrenExpanded(boolean expanded, boolean animate)729     public void setChildrenExpanded(boolean expanded, boolean animate) {
730         mChildrenExpanded = expanded;
731         updateChildrenVisibility(animate);
732     }
733 
updateExpandButton()734     public void updateExpandButton() {
735         boolean hasExpand = hasBottomDecor();
736         if (hasExpand != mHasExpandAction) {
737             if (hasExpand) {
738                 if (mExpandButtonContainer == null) {
739                     mExpandButtonStub.inflate();
740                 }
741                 mExpandButtonContainer.setVisibility(View.VISIBLE);
742                 updateExpandButtonAppearance();
743                 updateExpandButtonColor();
744             } else if (mExpandButtonContainer != null) {
745                 mExpandButtonContainer.setVisibility(View.GONE);
746             }
747             notifyHeightChanged(true  /* needsAnimation */);
748         }
749         mHasExpandAction = hasExpand;
750     }
751 
updateExpandButtonAppearance()752     private void updateExpandButtonAppearance() {
753         if (mExpandButtonContainer == null) {
754             return;
755         }
756         float expandButtonAlpha = 0.0f;
757         float expandButtonTranslation = 0.0f;
758         float containerTranslation = 0.0f;
759         int minHeight = getMinHeight();
760         if (!mChildrenExpanded || mChildExpandAnimator != null) {
761             int expandActionHeight = getBottomDecorHeight();
762             int translationY = getActualHeight() - expandActionHeight;
763             if (translationY > minHeight) {
764                 containerTranslation = translationY;
765                 expandButtonAlpha = 1.0f;
766                 expandButtonTranslation = 0.0f;
767             } else {
768                 containerTranslation = minHeight;
769                 float progress = expandActionHeight != 0
770                         ? (minHeight - translationY) / (float) expandActionHeight
771                         : 1.0f;
772                 expandButtonTranslation = -progress * expandActionHeight * 0.7f;
773                 float alphaProgress = Math.min(progress / 0.7f, 1.0f);
774                 alphaProgress = PhoneStatusBar.ALPHA_OUT.getInterpolation(alphaProgress);
775                 expandButtonAlpha = 1.0f - alphaProgress;
776             }
777         }
778         if (mChildExpandAnimator != null || mChildrenExpanded) {
779             expandButtonAlpha = (1.0f - mChildrenExpandProgress)
780                     * expandButtonAlpha;
781             expandButtonTranslation = (1.0f - mChildrenExpandProgress)
782                     * expandButtonTranslation;
783             float newTranslation = -getBottomDecorHeight();
784 
785             // We don't want to take the actual height of the view as this is already
786             // interpolated by a custom interpolator leading to a confusing animation. We want
787             // to have a stable end value to interpolate in between
788             float collapsedHeight = !mChildrenExpanded
789                     ? Math.max(StackStateAnimator.getFinalActualHeight(this)
790                             - getBottomDecorHeight(), minHeight)
791                     : mExpandButtonStart;
792             float translationProgress = mFastOutSlowInInterpolator.getInterpolation(
793                     mChildrenExpandProgress);
794             containerTranslation = (1.0f - translationProgress) * collapsedHeight
795                     + translationProgress * newTranslation;
796         }
797         mExpandButton.setAlpha(expandButtonAlpha);
798         mExpandButtonDivider.setAlpha(expandButtonAlpha);
799         mExpandButton.setTranslationY(expandButtonTranslation);
800         mExpandButtonContainer.setTranslationY(containerTranslation);
801         NotificationContentView showingLayout = getShowingLayout();
802         float layoutTranslation =
803                 mExpandButtonContainer.getTranslationY() - showingLayout.getContentHeight();
804         layoutTranslation = Math.min(layoutTranslation, 0);
805         if (!mChildrenExpanded && mChildExpandAnimator == null) {
806             // Needed for the DragDownHelper in order not to jump there, as the position
807             // can be negative for a short time.
808             layoutTranslation = 0;
809         }
810         showingLayout.setTranslationY(layoutTranslation);
811         if (mChildrenContainer != null) {
812             mChildrenContainer.setTranslationY(
813                     mExpandButtonContainer.getTranslationY() + getBottomDecorHeight());
814         }
815     }
816 
updateExpandButtonColor()817     private void updateExpandButtonColor() {
818         // TODO: This needs some more baking, currently only the divider is colored according to
819         // the tint, but legacy black doesn't work yet perfectly for the button etc.
820         int color = getRippleColor();
821         if (color == mNormalRippleColor) {
822             color = 0;
823         }
824         if (mExpandButtonDivider != null) {
825             applyTint(mExpandButtonDivider, color);
826         }
827         if (mChildrenContainer != null) {
828             mChildrenContainer.setTintColor(color);
829         }
830     }
831 
applyTint(View v, int color)832     public static void applyTint(View v, int color) {
833         int alpha;
834         if (color != 0) {
835             alpha = COLORED_DIVIDER_ALPHA;
836         } else {
837             color = 0xff000000;
838             alpha = DEFAULT_DIVIDER_ALPHA;
839         }
840         if (v.getBackground() instanceof ColorDrawable) {
841             ColorDrawable background = (ColorDrawable) v.getBackground();
842             background.mutate();
843             background.setColor(color);
844             background.setAlpha(alpha);
845         }
846     }
847 
getMaxExpandHeight()848     public int getMaxExpandHeight() {
849         return mMaxExpandHeight;
850     }
851 
852     @Override
isContentExpandable()853     public boolean isContentExpandable() {
854         NotificationContentView showingLayout = getShowingLayout();
855         return showingLayout.isContentExpandable();
856     }
857 
858     @Override
getContentView()859     protected View getContentView() {
860         return getShowingLayout();
861     }
862 
863     @Override
setActualHeight(int height, boolean notifyListeners)864     public void setActualHeight(int height, boolean notifyListeners) {
865         super.setActualHeight(height, notifyListeners);
866         int contentHeight = calculateContentHeightFromActualHeight(height);
867         mPrivateLayout.setContentHeight(contentHeight);
868         mPublicLayout.setContentHeight(contentHeight);
869         if (mGuts != null) {
870             mGuts.setActualHeight(height);
871         }
872         invalidate();
873         updateExpandButtonAppearance();
874     }
875 
876     @Override
getMaxContentHeight()877     public int getMaxContentHeight() {
878         NotificationContentView showingLayout = getShowingLayout();
879         return showingLayout.getMaxHeight();
880     }
881 
882     @Override
getMinHeight()883     public int getMinHeight() {
884         NotificationContentView showingLayout = getShowingLayout();
885         return showingLayout.getMinHeight();
886     }
887 
888     @Override
setClipTopAmount(int clipTopAmount)889     public void setClipTopAmount(int clipTopAmount) {
890         super.setClipTopAmount(clipTopAmount);
891         mPrivateLayout.setClipTopAmount(clipTopAmount);
892         mPublicLayout.setClipTopAmount(clipTopAmount);
893         if (mGuts != null) {
894             mGuts.setClipTopAmount(clipTopAmount);
895         }
896     }
897 
notifyContentUpdated()898     public void notifyContentUpdated() {
899         mPublicLayout.notifyContentUpdated();
900         mPrivateLayout.notifyContentUpdated();
901     }
902 
isMaxExpandHeightInitialized()903     public boolean isMaxExpandHeightInitialized() {
904         return mMaxExpandHeight != 0;
905     }
906 
getShowingLayout()907     private NotificationContentView getShowingLayout() {
908         return mShowingPublic ? mPublicLayout : mPrivateLayout;
909     }
910 
911     @Override
setShowingLegacyBackground(boolean showing)912     public void setShowingLegacyBackground(boolean showing) {
913         super.setShowingLegacyBackground(showing);
914         mPrivateLayout.setShowingLegacyBackground(showing);
915         mPublicLayout.setShowingLegacyBackground(showing);
916     }
917 
setExpansionLogger(ExpansionLogger logger, String key)918     public void setExpansionLogger(ExpansionLogger logger, String key) {
919         mLogger = logger;
920         mLoggingKey = key;
921     }
922 
logExpansionEvent(boolean userAction, boolean wasExpanded)923     private void logExpansionEvent(boolean userAction, boolean wasExpanded) {
924         final boolean nowExpanded = isExpanded();
925         if (wasExpanded != nowExpanded && mLogger != null) {
926             mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded) ;
927         }
928     }
929 }
930