• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.annotation.NonNull;
23 
24 import com.android.systemui.Dumpable;
25 import com.android.systemui.R;
26 import com.android.systemui.dagger.SysUISingleton;
27 import com.android.systemui.dump.DumpManager;
28 import com.android.systemui.flags.FeatureFlags;
29 import com.android.systemui.flags.Flags;
30 import com.android.systemui.statusbar.notification.LegacySourceType;
31 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
32 import com.android.systemui.statusbar.notification.Roundable;
33 import com.android.systemui.statusbar.notification.SourceType;
34 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
35 import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger;
36 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
37 import com.android.systemui.statusbar.notification.row.ExpandableView;
38 
39 import java.io.PrintWriter;
40 import java.util.HashSet;
41 
42 import javax.inject.Inject;
43 
44 /**
45  * A class that manages the roundness for notification views
46  */
47 @SysUISingleton
48 public class NotificationRoundnessManager implements Dumpable {
49 
50     private static final String TAG = "NotificationRoundnessManager";
51     private static final SourceType DISMISS_ANIMATION = SourceType.from("DismissAnimation");
52 
53     private final ExpandableView[] mFirstInSectionViews;
54     private final ExpandableView[] mLastInSectionViews;
55     private final ExpandableView[] mTmpFirstInSectionViews;
56     private final ExpandableView[] mTmpLastInSectionViews;
57     private final NotificationRoundnessLogger mNotifLogger;
58     private final DumpManager mDumpManager;
59     private boolean mExpanded;
60     private HashSet<ExpandableView> mAnimatedChildren;
61     private Runnable mRoundingChangedCallback;
62     private ExpandableNotificationRow mTrackedHeadsUp;
63     private float mAppearFraction;
64     private boolean mRoundForPulsingViews;
65     private boolean mIsClearAllInProgress;
66 
67     private ExpandableView mSwipedView = null;
68     private Roundable mViewBeforeSwipedView = null;
69     private Roundable mViewAfterSwipedView = null;
70     private boolean mUseRoundnessSourceTypes;
71 
72     @Inject
NotificationRoundnessManager( NotificationSectionsFeatureManager sectionsFeatureManager, NotificationRoundnessLogger notifLogger, DumpManager dumpManager, FeatureFlags featureFlags)73     NotificationRoundnessManager(
74             NotificationSectionsFeatureManager sectionsFeatureManager,
75             NotificationRoundnessLogger notifLogger,
76             DumpManager dumpManager,
77             FeatureFlags featureFlags) {
78         int numberOfSections = sectionsFeatureManager.getNumberOfBuckets();
79         mFirstInSectionViews = new ExpandableView[numberOfSections];
80         mLastInSectionViews = new ExpandableView[numberOfSections];
81         mTmpFirstInSectionViews = new ExpandableView[numberOfSections];
82         mTmpLastInSectionViews = new ExpandableView[numberOfSections];
83         mNotifLogger = notifLogger;
84         mDumpManager = dumpManager;
85         mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
86 
87         mDumpManager.registerDumpable(TAG, this);
88     }
89 
90     @Override
dump(@onNull PrintWriter pw, @NonNull String[] args)91     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
92         pw.println("mFirstInSectionViews: length=" + mFirstInSectionViews.length);
93         pw.println(dumpViews(mFirstInSectionViews));
94         pw.println("mLastInSectionViews: length=" + mLastInSectionViews.length);
95         pw.println(dumpViews(mFirstInSectionViews));
96         if (mTrackedHeadsUp != null) {
97             pw.println("trackedHeadsUp=" + mTrackedHeadsUp.getEntry());
98         }
99         pw.println("roundForPulsingViews=" + mRoundForPulsingViews);
100         pw.println("isClearAllInProgress=" + mIsClearAllInProgress);
101     }
102 
updateView(ExpandableView view, boolean animate)103     public void updateView(ExpandableView view, boolean animate) {
104         if (mUseRoundnessSourceTypes) return;
105         boolean changed = updateViewWithoutCallback(view, animate);
106         if (changed) {
107             mRoundingChangedCallback.run();
108         }
109     }
110 
isViewAffectedBySwipe(ExpandableView expandableView)111     public boolean isViewAffectedBySwipe(ExpandableView expandableView) {
112         return expandableView != null
113                 && (expandableView == mSwipedView
114                 || expandableView == mViewBeforeSwipedView
115                 || expandableView == mViewAfterSwipedView);
116     }
117 
updateViewWithoutCallback( ExpandableView view, boolean animate)118     boolean updateViewWithoutCallback(
119             ExpandableView view,
120             boolean animate) {
121         if (mUseRoundnessSourceTypes) return false;
122         if (view == null
123                 || view == mViewBeforeSwipedView
124                 || view == mViewAfterSwipedView) {
125             return false;
126         }
127 
128         final boolean isTopChanged = view.requestTopRoundness(
129                 getRoundnessDefaultValue(view, true /* top */),
130                 LegacySourceType.DefaultValue,
131                 animate);
132 
133         final boolean isBottomChanged = view.requestBottomRoundness(
134                 getRoundnessDefaultValue(view, /* top = */ false),
135                 LegacySourceType.DefaultValue,
136                 animate);
137 
138         final boolean isFirstInSection = isFirstInSection(view);
139         final boolean isLastInSection = isLastInSection(view);
140 
141         view.setFirstInSection(isFirstInSection);
142         view.setLastInSection(isLastInSection);
143 
144         mNotifLogger.onCornersUpdated(view, isFirstInSection,
145                 isLastInSection, isTopChanged, isBottomChanged);
146 
147         return (isFirstInSection || isLastInSection) && (isTopChanged || isBottomChanged);
148     }
149 
isFirstInSection(ExpandableView view)150     private boolean isFirstInSection(ExpandableView view) {
151         if (mUseRoundnessSourceTypes) return false;
152         for (int i = 0; i < mFirstInSectionViews.length; i++) {
153             if (view == mFirstInSectionViews[i]) {
154                 return true;
155             }
156         }
157         return false;
158     }
159 
isLastInSection(ExpandableView view)160     private boolean isLastInSection(ExpandableView view) {
161         if (mUseRoundnessSourceTypes) return false;
162         for (int i = mLastInSectionViews.length - 1; i >= 0; i--) {
163             if (view == mLastInSectionViews[i]) {
164                 return true;
165             }
166         }
167         return false;
168     }
169 
setViewsAffectedBySwipe( Roundable viewBefore, ExpandableView viewSwiped, Roundable viewAfter)170     void setViewsAffectedBySwipe(
171             Roundable viewBefore,
172             ExpandableView viewSwiped,
173             Roundable viewAfter) {
174         // This method requires you to change the roundness of the current View targets and reset
175         // the roundness of the old View targets (if any) to 0f.
176         // To avoid conflicts, it generates a set of old Views and removes the current Views
177         // from this set.
178         HashSet<Roundable> oldViews = new HashSet<>();
179         if (mViewBeforeSwipedView != null) oldViews.add(mViewBeforeSwipedView);
180         if (mSwipedView != null) oldViews.add(mSwipedView);
181         if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView);
182 
183         final SourceType source;
184         if (mUseRoundnessSourceTypes) {
185             source = DISMISS_ANIMATION;
186         } else {
187             source = LegacySourceType.OnDismissAnimation;
188         }
189 
190         mViewBeforeSwipedView = viewBefore;
191         if (viewBefore != null) {
192             oldViews.remove(viewBefore);
193             viewBefore.requestRoundness(/* top = */ 0f, /* bottom = */ 1f, source);
194         }
195 
196         mSwipedView = viewSwiped;
197         if (viewSwiped != null) {
198             oldViews.remove(viewSwiped);
199             viewSwiped.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, source);
200         }
201 
202         mViewAfterSwipedView = viewAfter;
203         if (viewAfter != null) {
204             oldViews.remove(viewAfter);
205             viewAfter.requestRoundness(/* top = */ 1f, /* bottom = */ 0f, source);
206         }
207 
208         // After setting the current Views, reset the views that are still present in the set.
209         for (Roundable oldView : oldViews) {
210             oldView.requestRoundnessReset(source);
211         }
212     }
213 
setClearAllInProgress(boolean isClearingAll)214     void setClearAllInProgress(boolean isClearingAll) {
215         mIsClearAllInProgress = isClearingAll;
216     }
217 
218     /**
219      * Check if "Clear all" notifications is in progress.
220      */
isClearAllInProgress()221     public boolean isClearAllInProgress() {
222         return mIsClearAllInProgress;
223     }
224 
225     /**
226      * Check if we can request the `Pulsing` roundness for notification.
227      */
shouldRoundNotificationPulsing()228     public boolean shouldRoundNotificationPulsing() {
229         return mRoundForPulsingViews;
230     }
231 
getRoundnessDefaultValue(Roundable view, boolean top)232     private float getRoundnessDefaultValue(Roundable view, boolean top) {
233         if (mUseRoundnessSourceTypes) return 0f;
234 
235         if (view == null) {
236             return 0f;
237         }
238         if (view == mViewBeforeSwipedView
239                 || view == mSwipedView
240                 || view == mViewAfterSwipedView) {
241             return 1f;
242         }
243         if (view instanceof ExpandableNotificationRow
244                 && ((ExpandableNotificationRow) view).canViewBeCleared()
245                 && mIsClearAllInProgress) {
246             return 1.0f;
247         }
248         if (view instanceof ExpandableView) {
249             ExpandableView expandableView = (ExpandableView) view;
250             if ((expandableView.isPinned()
251                     || (expandableView.isHeadsUpAnimatingAway()) && !mExpanded)) {
252                 return 1.0f;
253             }
254             if (isFirstInSection(expandableView) && top) {
255                 return 1.0f;
256             }
257             if (isLastInSection(expandableView) && !top) {
258                 return 1.0f;
259             }
260 
261             if (view == mTrackedHeadsUp) {
262                 // If we're pushing up on a headsup the appear fraction is < 0 and it needs to
263                 // still be rounded.
264                 return MathUtils.saturate(1.0f - mAppearFraction);
265             }
266             if (expandableView.showingPulsing() && mRoundForPulsingViews) {
267                 return 1.0f;
268             }
269             if (expandableView.isChildInGroup()) {
270                 return 0f;
271             }
272             final Resources resources = expandableView.getResources();
273             return resources.getDimension(R.dimen.notification_corner_radius_small)
274                     / resources.getDimension(R.dimen.notification_corner_radius);
275         }
276         return 0f;
277     }
278 
setExpanded(float expandedHeight, float appearFraction)279     public void setExpanded(float expandedHeight, float appearFraction) {
280         if (mUseRoundnessSourceTypes) return;
281         mExpanded = expandedHeight != 0.0f;
282         mAppearFraction = appearFraction;
283         if (mTrackedHeadsUp != null) {
284             updateView(mTrackedHeadsUp, false /* animate */);
285         }
286     }
287 
updateRoundedChildren(NotificationSection[] sections)288     public void updateRoundedChildren(NotificationSection[] sections) {
289         if (mUseRoundnessSourceTypes) return;
290         boolean anyChanged = false;
291         for (int i = 0; i < sections.length; i++) {
292             mTmpFirstInSectionViews[i] = mFirstInSectionViews[i];
293             mTmpLastInSectionViews[i] = mLastInSectionViews[i];
294             mFirstInSectionViews[i] = sections[i].getFirstVisibleChild();
295             mLastInSectionViews[i] = sections[i].getLastVisibleChild();
296         }
297         anyChanged |= handleRemovedOldViews(sections, mTmpFirstInSectionViews, true);
298         anyChanged |= handleRemovedOldViews(sections, mTmpLastInSectionViews, false);
299         anyChanged |= handleAddedNewViews(sections, mTmpFirstInSectionViews, true);
300         anyChanged |= handleAddedNewViews(sections, mTmpLastInSectionViews, false);
301         if (anyChanged) {
302             mRoundingChangedCallback.run();
303         }
304 
305         mNotifLogger.onSectionCornersUpdated(sections, anyChanged);
306     }
307 
handleRemovedOldViews( NotificationSection[] sections, ExpandableView[] oldViews, boolean first)308     private boolean handleRemovedOldViews(
309             NotificationSection[] sections,
310             ExpandableView[] oldViews,
311             boolean first) {
312         if (mUseRoundnessSourceTypes) return false;
313         boolean anyChanged = false;
314         for (ExpandableView oldView : oldViews) {
315             if (oldView != null) {
316                 boolean isStillPresent = false;
317                 boolean adjacentSectionChanged = false;
318                 for (NotificationSection section : sections) {
319                     ExpandableView newView =
320                             (first ? section.getFirstVisibleChild()
321                                     : section.getLastVisibleChild());
322                     if (newView == oldView) {
323                         isStillPresent = true;
324                         if (oldView.isFirstInSection() != isFirstInSection(oldView)
325                                 || oldView.isLastInSection() != isLastInSection(oldView)) {
326                             adjacentSectionChanged = true;
327                         }
328                         break;
329                     }
330                 }
331                 if (!isStillPresent || adjacentSectionChanged) {
332                     anyChanged = true;
333                     if (!oldView.isRemoved()) {
334                         updateViewWithoutCallback(oldView, oldView.isShown());
335                     }
336                 }
337             }
338         }
339         return anyChanged;
340     }
341 
handleAddedNewViews( NotificationSection[] sections, ExpandableView[] oldViews, boolean first)342     private boolean handleAddedNewViews(
343             NotificationSection[] sections,
344             ExpandableView[] oldViews,
345             boolean first) {
346         if (mUseRoundnessSourceTypes) return false;
347         boolean anyChanged = false;
348         for (NotificationSection section : sections) {
349             ExpandableView newView =
350                     (first ? section.getFirstVisibleChild() : section.getLastVisibleChild());
351             if (newView != null) {
352                 boolean wasAlreadyPresent = false;
353                 for (ExpandableView oldView : oldViews) {
354                     if (oldView == newView) {
355                         wasAlreadyPresent = true;
356                         break;
357                     }
358                 }
359                 if (!wasAlreadyPresent) {
360                     anyChanged = true;
361                     updateViewWithoutCallback(newView,
362                             newView.isShown() && !mAnimatedChildren.contains(newView));
363                 }
364             }
365         }
366         return anyChanged;
367     }
368 
setAnimatedChildren(HashSet<ExpandableView> animatedChildren)369     public void setAnimatedChildren(HashSet<ExpandableView> animatedChildren) {
370         mAnimatedChildren = animatedChildren;
371     }
372 
373     /**
374      * Check if the view should be animated
375      * @param view target view
376      * @return true, if is in the AnimatedChildren set
377      */
isAnimatedChild(ExpandableView view)378     public boolean isAnimatedChild(ExpandableView view) {
379         return mAnimatedChildren.contains(view);
380     }
381 
setOnRoundingChangedCallback(Runnable roundingChangedCallback)382     public void setOnRoundingChangedCallback(Runnable roundingChangedCallback) {
383         mRoundingChangedCallback = roundingChangedCallback;
384     }
385 
setTrackingHeadsUp(ExpandableNotificationRow row)386     public void setTrackingHeadsUp(ExpandableNotificationRow row) {
387         ExpandableNotificationRow previous = mTrackedHeadsUp;
388         mTrackedHeadsUp = row;
389         if (previous != null) {
390             updateView(previous, true /* animate */);
391         }
392     }
393 
setShouldRoundPulsingViews(boolean shouldRoundPulsingViews)394     public void setShouldRoundPulsingViews(boolean shouldRoundPulsingViews) {
395         mRoundForPulsingViews = shouldRoundPulsingViews;
396     }
397 
dumpViews(ExpandableView[] views)398     private String dumpViews(ExpandableView[] views) {
399         StringBuilder sb = new StringBuilder();
400         for (int i = 0; i < views.length; i++) {
401             if (views[i] == null) continue;
402 
403             sb.append("\t")
404                     .append("[").append(i).append("] ")
405                     .append("isPinned=").append(views[i].isPinned()).append(" ")
406                     .append("isFirstInSection=").append(views[i].isFirstInSection()).append(" ")
407                     .append("isLastInSection=").append(views[i].isLastInSection()).append(" ");
408 
409             if (views[i] instanceof ExpandableNotificationRow) {
410                 sb.append("entry=");
411                 dumpEntry(((ExpandableNotificationRow) views[i]).getEntry(), sb);
412             }
413 
414             sb.append("\n");
415         }
416         return sb.toString();
417     }
418 
dumpEntry(NotificationEntry entry, StringBuilder sb)419     private void dumpEntry(NotificationEntry entry, StringBuilder sb) {
420         sb.append("NotificationEntry{key=").append(entry.getKey()).append(" ");
421 
422         if (entry.getSection() != null) {
423             sb.append(" section=")
424                     .append(entry.getSection().getLabel());
425         }
426 
427         sb.append("}");
428     }
429 }
430