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.phone; 18 19 import android.graphics.Point; 20 import android.graphics.Rect; 21 import android.view.View; 22 import android.view.WindowInsets; 23 24 import com.android.internal.annotations.VisibleForTesting; 25 import com.android.systemui.Dependency; 26 import com.android.systemui.R; 27 import com.android.systemui.statusbar.CrossFadeHelper; 28 import com.android.systemui.statusbar.ExpandableNotificationRow; 29 import com.android.systemui.statusbar.HeadsUpStatusBarView; 30 import com.android.systemui.statusbar.NotificationData; 31 import com.android.systemui.statusbar.policy.DarkIconDispatcher; 32 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 33 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 34 35 import java.util.function.BiConsumer; 36 import java.util.function.Consumer; 37 38 /** 39 * Controls the appearance of heads up notifications in the icon area and the header itself. 40 */ 41 public class HeadsUpAppearanceController implements OnHeadsUpChangedListener, 42 DarkIconDispatcher.DarkReceiver { 43 public static final int CONTENT_FADE_DURATION = 110; 44 public static final int CONTENT_FADE_DELAY = 100; 45 private final NotificationIconAreaController mNotificationIconAreaController; 46 private final HeadsUpManagerPhone mHeadsUpManager; 47 private final NotificationStackScrollLayout mStackScroller; 48 private final HeadsUpStatusBarView mHeadsUpStatusBarView; 49 private final View mClockView; 50 private final DarkIconDispatcher mDarkIconDispatcher; 51 private final NotificationPanelView mPanelView; 52 private final Consumer<ExpandableNotificationRow> 53 mSetTrackingHeadsUp = this::setTrackingHeadsUp; 54 private final Runnable mUpdatePanelTranslation = this::updatePanelTranslation; 55 private final BiConsumer<Float, Float> mSetExpandedHeight = this::setExpandedHeight; 56 private float mExpandedHeight; 57 private boolean mIsExpanded; 58 private float mExpandFraction; 59 private ExpandableNotificationRow mTrackedChild; 60 private boolean mShown; 61 private final View.OnLayoutChangeListener mStackScrollLayoutChangeListener = 62 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) 63 -> updatePanelTranslation(); 64 Point mPoint; 65 66 public HeadsUpAppearanceController( 67 NotificationIconAreaController notificationIconAreaController, 68 HeadsUpManagerPhone headsUpManager, 69 View statusbarView) { 70 this(notificationIconAreaController, headsUpManager, 71 statusbarView.findViewById(R.id.heads_up_status_bar_view), 72 statusbarView.findViewById(R.id.notification_stack_scroller), 73 statusbarView.findViewById(R.id.notification_panel), 74 statusbarView.findViewById(R.id.clock)); 75 } 76 77 @VisibleForTesting 78 public HeadsUpAppearanceController( 79 NotificationIconAreaController notificationIconAreaController, 80 HeadsUpManagerPhone headsUpManager, 81 HeadsUpStatusBarView headsUpStatusBarView, 82 NotificationStackScrollLayout stackScroller, 83 NotificationPanelView panelView, 84 View clockView) { 85 mNotificationIconAreaController = notificationIconAreaController; 86 mHeadsUpManager = headsUpManager; 87 mHeadsUpManager.addListener(this); 88 mHeadsUpStatusBarView = headsUpStatusBarView; 89 headsUpStatusBarView.setOnDrawingRectChangedListener( 90 () -> updateIsolatedIconLocation(true /* requireUpdate */)); 91 mStackScroller = stackScroller; 92 mPanelView = panelView; 93 panelView.addTrackingHeadsUpListener(mSetTrackingHeadsUp); 94 panelView.addVerticalTranslationListener(mUpdatePanelTranslation); 95 panelView.setHeadsUpAppearanceController(this); 96 mStackScroller.addOnExpandedHeightListener(mSetExpandedHeight); 97 mStackScroller.addOnLayoutChangeListener(mStackScrollLayoutChangeListener); 98 mStackScroller.setHeadsUpAppearanceController(this); 99 mClockView = clockView; 100 mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class); 101 mDarkIconDispatcher.addDarkReceiver(this); 102 } 103 104 105 public void destroy() { 106 mHeadsUpManager.removeListener(this); 107 mHeadsUpStatusBarView.setOnDrawingRectChangedListener(null); 108 mPanelView.removeTrackingHeadsUpListener(mSetTrackingHeadsUp); 109 mPanelView.removeVerticalTranslationListener(mUpdatePanelTranslation); 110 mPanelView.setHeadsUpAppearanceController(null); 111 mStackScroller.removeOnExpandedHeightListener(mSetExpandedHeight); 112 mStackScroller.removeOnLayoutChangeListener(mStackScrollLayoutChangeListener); 113 mDarkIconDispatcher.removeDarkReceiver(this); 114 } 115 116 private void updateIsolatedIconLocation(boolean requireStateUpdate) { 117 mNotificationIconAreaController.setIsolatedIconLocation( 118 mHeadsUpStatusBarView.getIconDrawingRect(), requireStateUpdate); 119 } 120 121 @Override 122 public void onHeadsUpPinned(ExpandableNotificationRow headsUp) { 123 updateTopEntry(); 124 updateHeader(headsUp.getEntry()); 125 } 126 127 /** To count the distance from the window right boundary to scroller right boundary. The 128 * distance formula is the following: 129 * Y = screenSize - (SystemWindow's width + Scroller.getRight()) 130 * There are four modes MUST to be considered in Cut Out of RTL. 131 * No Cut Out: 132 * Scroller + NB 133 * NB + Scroller 134 * => SystemWindow = NavigationBar's width 135 * => Y = screenSize - (SystemWindow's width + Scroller.getRight()) 136 * Corner Cut Out or Tall Cut Out: 137 * cut out + Scroller + NB 138 * NB + Scroller + cut out 139 * => SystemWindow = NavigationBar's width 140 * => Y = screenSize - (SystemWindow's width + Scroller.getRight()) 141 * Double Cut Out: 142 * cut out left + Scroller + (NB + cut out right) 143 * SystemWindow = NavigationBar's width + cut out right width 144 * => Y = screenSize - (SystemWindow's width + Scroller.getRight()) 145 * (cut out left + NB) + Scroller + cut out right 146 * SystemWindow = NavigationBar's width + cut out left width 147 * => Y = screenSize - (SystemWindow's width + Scroller.getRight()) 148 * @return the translation X value for RTL. In theory, it should be negative. i.e. -Y 149 */ 150 private int getRtlTranslation() { 151 if (mPoint == null) { 152 mPoint = new Point(); 153 } 154 155 int realDisplaySize = 0; 156 if (mStackScroller.getDisplay() != null) { 157 mStackScroller.getDisplay().getRealSize(mPoint); 158 realDisplaySize = mPoint.x; 159 } 160 161 WindowInsets windowInset = mStackScroller.getRootWindowInsets(); 162 return windowInset.getSystemWindowInsetLeft() + mStackScroller.getRight() 163 + windowInset.getSystemWindowInsetRight() - realDisplaySize; 164 } 165 166 public void updatePanelTranslation() { 167 float newTranslation; 168 if (mStackScroller.isLayoutRtl()) { 169 newTranslation = getRtlTranslation(); 170 } else { 171 newTranslation = mStackScroller.getLeft(); 172 } 173 newTranslation += mStackScroller.getTranslationX(); 174 mHeadsUpStatusBarView.setPanelTranslation(newTranslation); 175 } 176 177 private void updateTopEntry() { 178 NotificationData.Entry newEntry = null; 179 if (!mIsExpanded && mHeadsUpManager.hasPinnedHeadsUp()) { 180 newEntry = mHeadsUpManager.getTopEntry(); 181 } 182 NotificationData.Entry previousEntry = mHeadsUpStatusBarView.getShowingEntry(); 183 mHeadsUpStatusBarView.setEntry(newEntry); 184 if (newEntry != previousEntry) { 185 boolean animateIsolation = false; 186 if (newEntry == null) { 187 // no heads up anymore, lets start the disappear animation 188 189 setShown(false); 190 animateIsolation = !mIsExpanded; 191 } else if (previousEntry == null) { 192 // We now have a headsUp and didn't have one before. Let's start the disappear 193 // animation 194 setShown(true); 195 animateIsolation = !mIsExpanded; 196 } 197 updateIsolatedIconLocation(false /* requireUpdate */); 198 mNotificationIconAreaController.showIconIsolated(newEntry == null ? null 199 : newEntry.icon, animateIsolation); 200 } 201 } 202 203 private void setShown(boolean isShown) { 204 if (mShown != isShown) { 205 mShown = isShown; 206 if (isShown) { 207 mHeadsUpStatusBarView.setVisibility(View.VISIBLE); 208 CrossFadeHelper.fadeIn(mHeadsUpStatusBarView, CONTENT_FADE_DURATION /* duration */, 209 CONTENT_FADE_DELAY /* delay */); 210 CrossFadeHelper.fadeOut(mClockView, CONTENT_FADE_DURATION/* duration */, 211 0 /* delay */, () -> mClockView.setVisibility(View.INVISIBLE)); 212 } else { 213 CrossFadeHelper.fadeIn(mClockView, CONTENT_FADE_DURATION /* duration */, 214 CONTENT_FADE_DELAY /* delay */); 215 CrossFadeHelper.fadeOut(mHeadsUpStatusBarView, CONTENT_FADE_DURATION/* duration */, 216 0 /* delay */, () -> mHeadsUpStatusBarView.setVisibility(View.GONE)); 217 218 } 219 } 220 } 221 222 @VisibleForTesting 223 public boolean isShown() { 224 return mShown; 225 } 226 227 /** 228 * Should the headsup status bar view be visible right now? This may be different from isShown, 229 * since the headsUp manager might not have notified us yet of the state change. 230 * 231 * @return if the heads up status bar view should be shown 232 */ 233 public boolean shouldBeVisible() { 234 return !mIsExpanded && mHeadsUpManager.hasPinnedHeadsUp(); 235 } 236 237 @Override 238 public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) { 239 updateTopEntry(); 240 updateHeader(headsUp.getEntry()); 241 } 242 243 public void setExpandedHeight(float expandedHeight, float appearFraction) { 244 boolean changedHeight = expandedHeight != mExpandedHeight; 245 mExpandedHeight = expandedHeight; 246 mExpandFraction = appearFraction; 247 boolean isExpanded = expandedHeight > 0; 248 if (changedHeight) { 249 updateHeadsUpHeaders(); 250 } 251 if (isExpanded != mIsExpanded) { 252 mIsExpanded = isExpanded; 253 updateTopEntry(); 254 } 255 } 256 257 /** 258 * Set a headsUp to be tracked, meaning that it is currently being pulled down after being 259 * in a pinned state on the top. The expand animation is different in that case and we need 260 * to update the header constantly afterwards. 261 * 262 * @param trackedChild the tracked headsUp or null if it's not tracking anymore. 263 */ 264 public void setTrackingHeadsUp(ExpandableNotificationRow trackedChild) { 265 ExpandableNotificationRow previousTracked = mTrackedChild; 266 mTrackedChild = trackedChild; 267 if (previousTracked != null) { 268 updateHeader(previousTracked.getEntry()); 269 } 270 } 271 272 private void updateHeadsUpHeaders() { 273 mHeadsUpManager.getAllEntries().forEach(entry -> { 274 updateHeader(entry); 275 }); 276 } 277 278 public void updateHeader(NotificationData.Entry entry) { 279 ExpandableNotificationRow row = entry.row; 280 float headerVisibleAmount = 1.0f; 281 if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild) { 282 headerVisibleAmount = mExpandFraction; 283 } 284 row.setHeaderVisibleAmount(headerVisibleAmount); 285 } 286 287 @Override 288 public void onDarkChanged(Rect area, float darkIntensity, int tint) { 289 mHeadsUpStatusBarView.onDarkChanged(area, darkIntensity, tint); 290 } 291 292 public void setPublicMode(boolean publicMode) { 293 mHeadsUpStatusBarView.setPublicMode(publicMode); 294 updateTopEntry(); 295 } 296 } 297