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