1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.systemui.statusbar.notification.stack;
18 
19 import android.util.MathUtils;
20 
21 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
22 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
23 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
24 import com.android.systemui.statusbar.notification.row.ExpandableView;
25 import com.android.systemui.statusbar.phone.KeyguardBypassController;
26 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
27 
28 import java.util.HashSet;
29 
30 import javax.inject.Inject;
31 import javax.inject.Singleton;
32 
33 /**
34  * A class that manages the roundness for notification views
35  */
36 @Singleton
37 public class NotificationRoundnessManager implements OnHeadsUpChangedListener {
38 
39     private final ExpandableView[] mFirstInSectionViews;
40     private final ExpandableView[] mLastInSectionViews;
41     private final ExpandableView[] mTmpFirstInSectionViews;
42     private final ExpandableView[] mTmpLastInSectionViews;
43     private final KeyguardBypassController mBypassController;
44     private boolean mExpanded;
45     private HashSet<ExpandableView> mAnimatedChildren;
46     private Runnable mRoundingChangedCallback;
47     private ExpandableNotificationRow mTrackedHeadsUp;
48     private float mAppearFraction;
49 
50     @Inject
NotificationRoundnessManager( KeyguardBypassController keyguardBypassController, NotificationSectionsFeatureManager sectionsFeatureManager)51     NotificationRoundnessManager(
52             KeyguardBypassController keyguardBypassController,
53             NotificationSectionsFeatureManager sectionsFeatureManager) {
54         int numberOfSections = sectionsFeatureManager.getNumberOfBuckets();
55         mFirstInSectionViews = new ExpandableView[numberOfSections];
56         mLastInSectionViews = new ExpandableView[numberOfSections];
57         mTmpFirstInSectionViews = new ExpandableView[numberOfSections];
58         mTmpLastInSectionViews = new ExpandableView[numberOfSections];
59         mBypassController = keyguardBypassController;
60     }
61 
62     @Override
onHeadsUpPinned(NotificationEntry headsUp)63     public void onHeadsUpPinned(NotificationEntry headsUp) {
64         updateView(headsUp.getRow(), false /* animate */);
65     }
66 
67     @Override
onHeadsUpUnPinned(NotificationEntry headsUp)68     public void onHeadsUpUnPinned(NotificationEntry headsUp) {
69         updateView(headsUp.getRow(), true /* animate */);
70     }
71 
onHeadsupAnimatingAwayChanged(ExpandableNotificationRow row, boolean isAnimatingAway)72     public void onHeadsupAnimatingAwayChanged(ExpandableNotificationRow row,
73             boolean isAnimatingAway) {
74         updateView(row, false /* animate */);
75     }
76 
77     @Override
onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp)78     public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
79         updateView(entry.getRow(), false /* animate */);
80     }
81 
updateView(ExpandableView view, boolean animate)82     private void updateView(ExpandableView view, boolean animate) {
83         boolean changed = updateViewWithoutCallback(view, animate);
84         if (changed) {
85             mRoundingChangedCallback.run();
86         }
87     }
88 
updateViewWithoutCallback(ExpandableView view, boolean animate)89     private boolean updateViewWithoutCallback(ExpandableView view,
90             boolean animate) {
91         float topRoundness = getRoundness(view, true /* top */);
92         float bottomRoundness = getRoundness(view, false /* top */);
93         boolean topChanged = view.setTopRoundness(topRoundness, animate);
94         boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate);
95         boolean firstInSection = isFirstInSection(view, false /* exclude first section */);
96         boolean lastInSection = isLastInSection(view, false /* exclude last section */);
97         view.setFirstInSection(firstInSection);
98         view.setLastInSection(lastInSection);
99         return (firstInSection || lastInSection) && (topChanged || bottomChanged);
100     }
101 
isFirstInSection(ExpandableView view, boolean includeFirstSection)102     private boolean isFirstInSection(ExpandableView view, boolean includeFirstSection) {
103         int numNonEmptySections = 0;
104         for (int i = 0; i < mFirstInSectionViews.length; i++) {
105             if (view == mFirstInSectionViews[i]) {
106                 return includeFirstSection || numNonEmptySections > 0;
107             }
108             if (mFirstInSectionViews[i] != null) {
109                 numNonEmptySections++;
110             }
111         }
112         return false;
113     }
114 
isLastInSection(ExpandableView view, boolean includeLastSection)115     private boolean isLastInSection(ExpandableView view, boolean includeLastSection) {
116         int numNonEmptySections = 0;
117         for (int i = mLastInSectionViews.length - 1; i >= 0; i--) {
118             if (view == mLastInSectionViews[i]) {
119                 return includeLastSection || numNonEmptySections > 0;
120             }
121             if (mLastInSectionViews[i] != null) {
122                 numNonEmptySections++;
123             }
124         }
125         return false;
126     }
127 
getRoundness(ExpandableView view, boolean top)128     private float getRoundness(ExpandableView view, boolean top) {
129         if ((view.isPinned() || view.isHeadsUpAnimatingAway()) && !mExpanded) {
130             return 1.0f;
131         }
132         if (isFirstInSection(view, true /* include first section */) && top) {
133             return 1.0f;
134         }
135         if (isLastInSection(view, true /* include last section */) && !top) {
136             return 1.0f;
137         }
138         if (view == mTrackedHeadsUp) {
139             // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be
140             // rounded.
141             return MathUtils.saturate(1.0f - mAppearFraction);
142         }
143         if (view.showingPulsing() && !mBypassController.getBypassEnabled()) {
144             return 1.0f;
145         }
146         return 0.0f;
147     }
148 
setExpanded(float expandedHeight, float appearFraction)149     public void setExpanded(float expandedHeight, float appearFraction) {
150         mExpanded = expandedHeight != 0.0f;
151         mAppearFraction = appearFraction;
152         if (mTrackedHeadsUp != null) {
153             updateView(mTrackedHeadsUp, true);
154         }
155     }
156 
updateRoundedChildren(NotificationSection[] sections)157     public void updateRoundedChildren(NotificationSection[] sections) {
158         boolean anyChanged = false;
159         for (int i = 0; i < sections.length; i++) {
160             mTmpFirstInSectionViews[i] = mFirstInSectionViews[i];
161             mTmpLastInSectionViews[i] = mLastInSectionViews[i];
162             mFirstInSectionViews[i] = sections[i].getFirstVisibleChild();
163             mLastInSectionViews[i] = sections[i].getLastVisibleChild();
164         }
165         anyChanged |= handleRemovedOldViews(sections, mTmpFirstInSectionViews, true);
166         anyChanged |= handleRemovedOldViews(sections, mTmpLastInSectionViews, false);
167         anyChanged |= handleAddedNewViews(sections, mTmpFirstInSectionViews, true);
168         anyChanged |= handleAddedNewViews(sections, mTmpLastInSectionViews, false);
169         if (anyChanged) {
170             mRoundingChangedCallback.run();
171         }
172     }
173 
handleRemovedOldViews(NotificationSection[] sections, ExpandableView[] oldViews, boolean first)174     private boolean handleRemovedOldViews(NotificationSection[] sections,
175             ExpandableView[] oldViews, boolean first) {
176         boolean anyChanged = false;
177         for (ExpandableView oldView : oldViews) {
178             if (oldView != null) {
179                 boolean isStillPresent = false;
180                 boolean adjacentSectionChanged = false;
181                 for (NotificationSection section : sections) {
182                     ExpandableView newView =
183                             (first ? section.getFirstVisibleChild()
184                                     : section.getLastVisibleChild());
185                     if (newView == oldView) {
186                         isStillPresent = true;
187                         if (oldView.isFirstInSection() != isFirstInSection(oldView,
188                                 false /* exclude first section */)
189                                 || oldView.isLastInSection() != isLastInSection(oldView,
190                                 false /* exclude last section */)) {
191                             adjacentSectionChanged = true;
192                         }
193                         break;
194                     }
195                 }
196                 if (!isStillPresent || adjacentSectionChanged) {
197                     anyChanged = true;
198                     if (!oldView.isRemoved()) {
199                         updateViewWithoutCallback(oldView, oldView.isShown());
200                     }
201                 }
202             }
203         }
204         return anyChanged;
205     }
206 
handleAddedNewViews(NotificationSection[] sections, ExpandableView[] oldViews, boolean first)207     private boolean handleAddedNewViews(NotificationSection[] sections,
208             ExpandableView[] oldViews, boolean first) {
209         boolean anyChanged = false;
210         for (NotificationSection section : sections) {
211             ExpandableView newView =
212                     (first ? section.getFirstVisibleChild() : section.getLastVisibleChild());
213             if (newView != null) {
214                 boolean wasAlreadyPresent = false;
215                 for (ExpandableView oldView : oldViews) {
216                     if (oldView == newView) {
217                         wasAlreadyPresent = true;
218                         break;
219                     }
220                 }
221                 if (!wasAlreadyPresent) {
222                     anyChanged = true;
223                     updateViewWithoutCallback(newView,
224                             newView.isShown() && !mAnimatedChildren.contains(newView));
225                 }
226             }
227         }
228         return anyChanged;
229     }
230 
setAnimatedChildren(HashSet<ExpandableView> animatedChildren)231     public void setAnimatedChildren(HashSet<ExpandableView> animatedChildren) {
232         mAnimatedChildren = animatedChildren;
233     }
234 
setOnRoundingChangedCallback(Runnable roundingChangedCallback)235     public void setOnRoundingChangedCallback(Runnable roundingChangedCallback) {
236         mRoundingChangedCallback = roundingChangedCallback;
237     }
238 
setTrackingHeadsUp(ExpandableNotificationRow row)239     public void setTrackingHeadsUp(ExpandableNotificationRow row) {
240         ExpandableNotificationRow previous = mTrackedHeadsUp;
241         mTrackedHeadsUp = row;
242         if (previous != null) {
243             updateView(previous, true /* animate */);
244         }
245     }
246 }
247