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 HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, View statusbarView)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 HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, HeadsUpStatusBarView headsUpStatusBarView, NotificationStackScrollLayout stackScroller, NotificationPanelView panelView, View clockView)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 destroy()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 updateIsolatedIconLocation(boolean requireStateUpdate)116 private void updateIsolatedIconLocation(boolean requireStateUpdate) { 117 mNotificationIconAreaController.setIsolatedIconLocation( 118 mHeadsUpStatusBarView.getIconDrawingRect(), requireStateUpdate); 119 } 120 121 @Override onHeadsUpPinned(ExpandableNotificationRow headsUp)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 */ getRtlTranslation()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 updatePanelTranslation()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 updateTopEntry()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 setShown(boolean isShown)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 isShown()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 */ shouldBeVisible()233 public boolean shouldBeVisible() { 234 return !mIsExpanded && mHeadsUpManager.hasPinnedHeadsUp(); 235 } 236 237 @Override onHeadsUpUnPinned(ExpandableNotificationRow headsUp)238 public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) { 239 updateTopEntry(); 240 updateHeader(headsUp.getEntry()); 241 } 242 setExpandedHeight(float expandedHeight, float appearFraction)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 */ setTrackingHeadsUp(ExpandableNotificationRow trackedChild)264 public void setTrackingHeadsUp(ExpandableNotificationRow trackedChild) { 265 ExpandableNotificationRow previousTracked = mTrackedChild; 266 mTrackedChild = trackedChild; 267 if (previousTracked != null) { 268 updateHeader(previousTracked.getEntry()); 269 } 270 } 271 updateHeadsUpHeaders()272 private void updateHeadsUpHeaders() { 273 mHeadsUpManager.getAllEntries().forEach(entry -> { 274 updateHeader(entry); 275 }); 276 } 277 updateHeader(NotificationData.Entry entry)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 onDarkChanged(Rect area, float darkIntensity, int tint)288 public void onDarkChanged(Rect area, float darkIntensity, int tint) { 289 mHeadsUpStatusBarView.onDarkChanged(area, darkIntensity, tint); 290 } 291 setPublicMode(boolean publicMode)292 public void setPublicMode(boolean publicMode) { 293 mHeadsUpStatusBarView.setPublicMode(publicMode); 294 updateTopEntry(); 295 } 296 } 297