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