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