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.content.res.Resources;
20 import android.util.MathUtils;
21 
22 import com.android.systemui.R;
23 import com.android.systemui.dagger.SysUISingleton;
24 import com.android.systemui.flags.FeatureFlags;
25 import com.android.systemui.flags.Flags;
26 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
27 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
28 import com.android.systemui.statusbar.notification.row.ExpandableView;
29 import com.android.systemui.statusbar.phone.KeyguardBypassController;
30 
31 import java.util.HashSet;
32 
33 import javax.inject.Inject;
34 
35 /**
36  * A class that manages the roundness for notification views
37  */
38 @SysUISingleton
39 public class NotificationRoundnessManager {
40 
41     private final ExpandableView[] mFirstInSectionViews;
42     private final ExpandableView[] mLastInSectionViews;
43     private final ExpandableView[] mTmpFirstInSectionViews;
44     private final ExpandableView[] mTmpLastInSectionViews;
45     private final KeyguardBypassController mBypassController;
46     private final FeatureFlags mFeatureFlags;
47     private boolean mExpanded;
48     private HashSet<ExpandableView> mAnimatedChildren;
49     private Runnable mRoundingChangedCallback;
50     private ExpandableNotificationRow mTrackedHeadsUp;
51     private float mAppearFraction;
52     private boolean mIsDismissAllInProgress;
53 
54     private ExpandableView mSwipedView = null;
55     private ExpandableView mViewBeforeSwipedView = null;
56     private ExpandableView mViewAfterSwipedView = null;
57 
58     @Inject
NotificationRoundnessManager( KeyguardBypassController keyguardBypassController, NotificationSectionsFeatureManager sectionsFeatureManager, FeatureFlags featureFlags)59     NotificationRoundnessManager(
60             KeyguardBypassController keyguardBypassController,
61             NotificationSectionsFeatureManager sectionsFeatureManager,
62             FeatureFlags featureFlags) {
63         mFeatureFlags = featureFlags;
64         int numberOfSections = sectionsFeatureManager.getNumberOfBuckets();
65         mFirstInSectionViews = new ExpandableView[numberOfSections];
66         mLastInSectionViews = new ExpandableView[numberOfSections];
67         mTmpFirstInSectionViews = new ExpandableView[numberOfSections];
68         mTmpLastInSectionViews = new ExpandableView[numberOfSections];
69         mBypassController = keyguardBypassController;
70     }
71 
updateView(ExpandableView view, boolean animate)72     public void updateView(ExpandableView view, boolean animate) {
73         boolean changed = updateViewWithoutCallback(view, animate);
74         if (changed) {
75             mRoundingChangedCallback.run();
76         }
77     }
78 
isViewAffectedBySwipe(ExpandableView expandableView)79     public boolean isViewAffectedBySwipe(ExpandableView expandableView) {
80         return expandableView != null
81                 && (expandableView == mSwipedView
82                     || expandableView == mViewBeforeSwipedView
83                     || expandableView == mViewAfterSwipedView);
84     }
85 
updateViewWithoutCallback(ExpandableView view, boolean animate)86     boolean updateViewWithoutCallback(ExpandableView view,
87             boolean animate) {
88         if (view == null
89                 || view == mViewBeforeSwipedView
90                 || view == mViewAfterSwipedView) {
91             return false;
92         }
93 
94         final float topRoundness = getRoundness(view, true /* top */);
95         final float bottomRoundness = getRoundness(view, false /* top */);
96 
97         final boolean topChanged = view.setTopRoundness(topRoundness, animate);
98         final boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate);
99 
100         final boolean isFirstInSection = isFirstInSection(view);
101         final boolean isLastInSection = isLastInSection(view);
102 
103         view.setFirstInSection(isFirstInSection);
104         view.setLastInSection(isLastInSection);
105 
106         return (isFirstInSection || isLastInSection) && (topChanged || bottomChanged);
107     }
108 
isFirstInSection(ExpandableView view)109     private boolean isFirstInSection(ExpandableView view) {
110         for (int i = 0; i < mFirstInSectionViews.length; i++) {
111             if (view == mFirstInSectionViews[i]) {
112                 return true;
113             }
114         }
115         return false;
116     }
117 
isLastInSection(ExpandableView view)118     private boolean isLastInSection(ExpandableView view) {
119         for (int i = mLastInSectionViews.length - 1; i >= 0; i--) {
120             if (view == mLastInSectionViews[i]) {
121                 return true;
122             }
123         }
124         return false;
125     }
126 
setViewsAffectedBySwipe( ExpandableView viewBefore, ExpandableView viewSwiped, ExpandableView viewAfter)127     void setViewsAffectedBySwipe(
128             ExpandableView viewBefore,
129             ExpandableView viewSwiped,
130             ExpandableView viewAfter) {
131         if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_UPDATES)) {
132             return;
133         }
134         final boolean animate = true;
135 
136         ExpandableView oldViewBefore = mViewBeforeSwipedView;
137         mViewBeforeSwipedView = viewBefore;
138         if (oldViewBefore != null) {
139             final float bottomRoundness = getRoundness(oldViewBefore, false /* top */);
140             oldViewBefore.setBottomRoundness(bottomRoundness,  animate);
141         }
142         if (viewBefore != null) {
143             viewBefore.setBottomRoundness(1f, animate);
144         }
145 
146         ExpandableView oldSwipedview = mSwipedView;
147         mSwipedView = viewSwiped;
148         if (oldSwipedview != null) {
149             final float bottomRoundness = getRoundness(oldSwipedview, false /* top */);
150             final float topRoundness = getRoundness(oldSwipedview, true /* top */);
151             oldSwipedview.setTopRoundness(topRoundness, animate);
152             oldSwipedview.setBottomRoundness(bottomRoundness, animate);
153         }
154         if (viewSwiped != null) {
155             viewSwiped.setTopRoundness(1f, animate);
156             viewSwiped.setBottomRoundness(1f, animate);
157         }
158 
159         ExpandableView oldViewAfter = mViewAfterSwipedView;
160         mViewAfterSwipedView = viewAfter;
161         if (oldViewAfter != null) {
162             final float topRoundness = getRoundness(oldViewAfter, true /* top */);
163             oldViewAfter.setTopRoundness(topRoundness, animate);
164         }
165         if (viewAfter != null) {
166             viewAfter.setTopRoundness(1f, animate);
167         }
168     }
169 
setDismissAllInProgress(boolean isClearingAll)170     void setDismissAllInProgress(boolean isClearingAll) {
171         mIsDismissAllInProgress = isClearingAll;
172     }
173 
getRoundness(ExpandableView view, boolean top)174     private float getRoundness(ExpandableView view, boolean top) {
175         if (view == null) {
176             return 0f;
177         }
178         if (view == mViewBeforeSwipedView
179                 || view == mSwipedView
180                 || view == mViewAfterSwipedView) {
181             return 1f;
182         }
183         if (view instanceof ExpandableNotificationRow
184                 && ((ExpandableNotificationRow) view).canViewBeDismissed()
185                 && mIsDismissAllInProgress) {
186             return 1.0f;
187         }
188         if ((view.isPinned()
189                 || (view.isHeadsUpAnimatingAway()) && !mExpanded)) {
190             return 1.0f;
191         }
192         if (isFirstInSection(view) && top) {
193             return 1.0f;
194         }
195         if (isLastInSection(view) && !top) {
196             return 1.0f;
197         }
198         if (view == mTrackedHeadsUp) {
199             // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be
200             // rounded.
201             return MathUtils.saturate(1.0f - mAppearFraction);
202         }
203         if (view.showingPulsing() && !mBypassController.getBypassEnabled()) {
204             return 1.0f;
205         }
206         final Resources resources = view.getResources();
207         return resources.getDimension(R.dimen.notification_corner_radius_small)
208                 / resources.getDimension(R.dimen.notification_corner_radius);
209     }
210 
setExpanded(float expandedHeight, float appearFraction)211     public void setExpanded(float expandedHeight, float appearFraction) {
212         mExpanded = expandedHeight != 0.0f;
213         mAppearFraction = appearFraction;
214         if (mTrackedHeadsUp != null) {
215             updateView(mTrackedHeadsUp, false /* animate */);
216         }
217     }
218 
updateRoundedChildren(NotificationSection[] sections)219     public void updateRoundedChildren(NotificationSection[] sections) {
220         boolean anyChanged = false;
221         for (int i = 0; i < sections.length; i++) {
222             mTmpFirstInSectionViews[i] = mFirstInSectionViews[i];
223             mTmpLastInSectionViews[i] = mLastInSectionViews[i];
224             mFirstInSectionViews[i] = sections[i].getFirstVisibleChild();
225             mLastInSectionViews[i] = sections[i].getLastVisibleChild();
226         }
227         anyChanged |= handleRemovedOldViews(sections, mTmpFirstInSectionViews, true);
228         anyChanged |= handleRemovedOldViews(sections, mTmpLastInSectionViews, false);
229         anyChanged |= handleAddedNewViews(sections, mTmpFirstInSectionViews, true);
230         anyChanged |= handleAddedNewViews(sections, mTmpLastInSectionViews, false);
231         if (anyChanged) {
232             mRoundingChangedCallback.run();
233         }
234     }
235 
handleRemovedOldViews(NotificationSection[] sections, ExpandableView[] oldViews, boolean first)236     private boolean handleRemovedOldViews(NotificationSection[] sections,
237             ExpandableView[] oldViews, boolean first) {
238         boolean anyChanged = false;
239         for (ExpandableView oldView : oldViews) {
240             if (oldView != null) {
241                 boolean isStillPresent = false;
242                 boolean adjacentSectionChanged = false;
243                 for (NotificationSection section : sections) {
244                     ExpandableView newView =
245                             (first ? section.getFirstVisibleChild()
246                                     : section.getLastVisibleChild());
247                     if (newView == oldView) {
248                         isStillPresent = true;
249                         if (oldView.isFirstInSection() != isFirstInSection(oldView)
250                                 || oldView.isLastInSection() != isLastInSection(oldView)) {
251                             adjacentSectionChanged = true;
252                         }
253                         break;
254                     }
255                 }
256                 if (!isStillPresent || adjacentSectionChanged) {
257                     anyChanged = true;
258                     if (!oldView.isRemoved()) {
259                         updateViewWithoutCallback(oldView, oldView.isShown());
260                     }
261                 }
262             }
263         }
264         return anyChanged;
265     }
266 
handleAddedNewViews(NotificationSection[] sections, ExpandableView[] oldViews, boolean first)267     private boolean handleAddedNewViews(NotificationSection[] sections,
268             ExpandableView[] oldViews, boolean first) {
269         boolean anyChanged = false;
270         for (NotificationSection section : sections) {
271             ExpandableView newView =
272                     (first ? section.getFirstVisibleChild() : section.getLastVisibleChild());
273             if (newView != null) {
274                 boolean wasAlreadyPresent = false;
275                 for (ExpandableView oldView : oldViews) {
276                     if (oldView == newView) {
277                         wasAlreadyPresent = true;
278                         break;
279                     }
280                 }
281                 if (!wasAlreadyPresent) {
282                     anyChanged = true;
283                     updateViewWithoutCallback(newView,
284                             newView.isShown() && !mAnimatedChildren.contains(newView));
285                 }
286             }
287         }
288         return anyChanged;
289     }
290 
setAnimatedChildren(HashSet<ExpandableView> animatedChildren)291     public void setAnimatedChildren(HashSet<ExpandableView> animatedChildren) {
292         mAnimatedChildren = animatedChildren;
293     }
294 
setOnRoundingChangedCallback(Runnable roundingChangedCallback)295     public void setOnRoundingChangedCallback(Runnable roundingChangedCallback) {
296         mRoundingChangedCallback = roundingChangedCallback;
297     }
298 
setTrackingHeadsUp(ExpandableNotificationRow row)299     public void setTrackingHeadsUp(ExpandableNotificationRow row) {
300         ExpandableNotificationRow previous = mTrackedHeadsUp;
301         mTrackedHeadsUp = row;
302         if (previous != null) {
303             updateView(previous, true /* animate */);
304         }
305     }
306 }
307