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 static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.NUM_SECTIONS;
20 
21 
22 import android.util.MathUtils;
23 
24 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
25 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
26 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
27 import com.android.systemui.statusbar.notification.row.ExpandableView;
28 import com.android.systemui.statusbar.phone.KeyguardBypassController;
29 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
30 
31 import java.util.HashSet;
32 
33 import javax.inject.Inject;
34 import javax.inject.Singleton;
35 
36 /**
37  * A class that manages the roundness for notification views
38  */
39 @Singleton
40 public class NotificationRoundnessManager implements OnHeadsUpChangedListener {
41 
42     private final ActivatableNotificationView[] mFirstInSectionViews;
43     private final ActivatableNotificationView[] mLastInSectionViews;
44     private final ActivatableNotificationView[] mTmpFirstInSectionViews;
45     private final ActivatableNotificationView[] mTmpLastInSectionViews;
46     private final KeyguardBypassController mBypassController;
47     private boolean mExpanded;
48     private HashSet<ExpandableView> mAnimatedChildren;
49     private Runnable mRoundingChangedCallback;
50     private ExpandableNotificationRow mTrackedHeadsUp;
51     private float mAppearFraction;
52 
53     @Inject
NotificationRoundnessManager(KeyguardBypassController keyguardBypassController)54     NotificationRoundnessManager(KeyguardBypassController keyguardBypassController) {
55         mFirstInSectionViews = new ActivatableNotificationView[NUM_SECTIONS];
56         mLastInSectionViews = new ActivatableNotificationView[NUM_SECTIONS];
57         mTmpFirstInSectionViews = new ActivatableNotificationView[NUM_SECTIONS];
58         mTmpLastInSectionViews = new ActivatableNotificationView[NUM_SECTIONS];
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(ActivatableNotificationView view, boolean animate)82     private void updateView(ActivatableNotificationView view, boolean animate) {
83         boolean changed = updateViewWithoutCallback(view, animate);
84         if (changed) {
85             mRoundingChangedCallback.run();
86         }
87     }
88 
updateViewWithoutCallback(ActivatableNotificationView view, boolean animate)89     private boolean updateViewWithoutCallback(ActivatableNotificationView 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(ActivatableNotificationView view, boolean includeFirstSection)102     private boolean isFirstInSection(ActivatableNotificationView view,
103             boolean includeFirstSection) {
104         int numNonEmptySections = 0;
105         for (int i = 0; i < mFirstInSectionViews.length; i++) {
106             if (view == mFirstInSectionViews[i]) {
107                 return includeFirstSection || numNonEmptySections > 0;
108             }
109             if (mFirstInSectionViews[i] != null) {
110                 numNonEmptySections++;
111             }
112         }
113         return false;
114     }
115 
isLastInSection(ActivatableNotificationView view, boolean includeLastSection)116     private boolean isLastInSection(ActivatableNotificationView view, boolean includeLastSection) {
117         int numNonEmptySections = 0;
118         for (int i = mLastInSectionViews.length - 1; i >= 0; i--) {
119             if (view == mLastInSectionViews[i]) {
120                 return includeLastSection || numNonEmptySections > 0;
121             }
122             if (mLastInSectionViews[i] != null) {
123                 numNonEmptySections++;
124             }
125         }
126         return false;
127     }
128 
getRoundness(ActivatableNotificationView view, boolean top)129     private float getRoundness(ActivatableNotificationView view, boolean top) {
130         if ((view.isPinned() || view.isHeadsUpAnimatingAway()) && !mExpanded) {
131             return 1.0f;
132         }
133         if (isFirstInSection(view, true /* include first section */) && top) {
134             return 1.0f;
135         }
136         if (isLastInSection(view, true /* include last section */) && !top) {
137             return 1.0f;
138         }
139         if (view == mTrackedHeadsUp) {
140             // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be
141             // rounded.
142             return MathUtils.saturate(1.0f - mAppearFraction);
143         }
144         if (view.showingPulsing() && !mBypassController.getBypassEnabled()) {
145             return 1.0f;
146         }
147         return 0.0f;
148     }
149 
setExpanded(float expandedHeight, float appearFraction)150     public void setExpanded(float expandedHeight, float appearFraction) {
151         mExpanded = expandedHeight != 0.0f;
152         mAppearFraction = appearFraction;
153         if (mTrackedHeadsUp != null) {
154             updateView(mTrackedHeadsUp, true);
155         }
156     }
157 
updateRoundedChildren(NotificationSection[] sections)158     public void updateRoundedChildren(NotificationSection[] sections) {
159         boolean anyChanged = false;
160         for (int i = 0; i < NUM_SECTIONS; i++) {
161             mTmpFirstInSectionViews[i] = mFirstInSectionViews[i];
162             mTmpLastInSectionViews[i] = mLastInSectionViews[i];
163             mFirstInSectionViews[i] = sections[i].getFirstVisibleChild();
164             mLastInSectionViews[i] = sections[i].getLastVisibleChild();
165         }
166         anyChanged |= handleRemovedOldViews(sections, mTmpFirstInSectionViews, true);
167         anyChanged |= handleRemovedOldViews(sections, mTmpLastInSectionViews, false);
168         anyChanged |= handleAddedNewViews(sections, mTmpFirstInSectionViews, true);
169         anyChanged |= handleAddedNewViews(sections, mTmpLastInSectionViews, false);
170         if (anyChanged) {
171             mRoundingChangedCallback.run();
172         }
173     }
174 
handleRemovedOldViews(NotificationSection[] sections, ActivatableNotificationView[] oldViews, boolean first)175     private boolean handleRemovedOldViews(NotificationSection[] sections,
176             ActivatableNotificationView[] oldViews, boolean first) {
177         boolean anyChanged = false;
178         for (ActivatableNotificationView oldView : oldViews) {
179             if (oldView != null) {
180                 boolean isStillPresent = false;
181                 boolean adjacentSectionChanged = false;
182                 for (NotificationSection section : sections) {
183                     ActivatableNotificationView newView =
184                             (first ? section.getFirstVisibleChild()
185                                     : section.getLastVisibleChild());
186                     if (newView == oldView) {
187                         isStillPresent = true;
188                         if (oldView.isFirstInSection() != isFirstInSection(oldView,
189                                 false /* exclude first section */)
190                                 || oldView.isLastInSection() != isLastInSection(oldView,
191                                 false /* exclude last section */)) {
192                             adjacentSectionChanged = true;
193                         }
194                         break;
195                     }
196                 }
197                 if (!isStillPresent || adjacentSectionChanged) {
198                     anyChanged = true;
199                     if (!oldView.isRemoved()) {
200                         updateViewWithoutCallback(oldView, oldView.isShown());
201                     }
202                 }
203             }
204         }
205         return anyChanged;
206     }
207 
handleAddedNewViews(NotificationSection[] sections, ActivatableNotificationView[] oldViews, boolean first)208     private boolean handleAddedNewViews(NotificationSection[] sections,
209             ActivatableNotificationView[] oldViews, boolean first) {
210         boolean anyChanged = false;
211         for (NotificationSection section : sections) {
212             ActivatableNotificationView newView =
213                     (first ? section.getFirstVisibleChild() : section.getLastVisibleChild());
214             if (newView != null) {
215                 boolean wasAlreadyPresent = false;
216                 for (ActivatableNotificationView oldView : oldViews) {
217                     if (oldView == newView) {
218                         wasAlreadyPresent = true;
219                         break;
220                     }
221                 }
222                 if (!wasAlreadyPresent) {
223                     anyChanged = true;
224                     updateViewWithoutCallback(newView,
225                             newView.isShown() && !mAnimatedChildren.contains(newView));
226                 }
227             }
228         }
229         return anyChanged;
230     }
231 
setAnimatedChildren(HashSet<ExpandableView> animatedChildren)232     public void setAnimatedChildren(HashSet<ExpandableView> animatedChildren) {
233         mAnimatedChildren = animatedChildren;
234     }
235 
setOnRoundingChangedCallback(Runnable roundingChangedCallback)236     public void setOnRoundingChangedCallback(Runnable roundingChangedCallback) {
237         mRoundingChangedCallback = roundingChangedCallback;
238     }
239 
setTrackingHeadsUp(ExpandableNotificationRow row)240     public void setTrackingHeadsUp(ExpandableNotificationRow row) {
241         ExpandableNotificationRow previous = mTrackedHeadsUp;
242         mTrackedHeadsUp = row;
243         if (previous != null) {
244             updateView(previous, true /* animate */);
245         }
246     }
247 }
248