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 static com.android.systemui.SysUiServiceProvider.getComponent; 20 21 import android.graphics.Point; 22 import android.graphics.Rect; 23 import android.view.DisplayCutout; 24 import android.view.View; 25 import android.view.WindowInsets; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.internal.widget.ViewClippingUtil; 29 import com.android.systemui.Dependency; 30 import com.android.systemui.R; 31 import com.android.systemui.plugins.DarkIconDispatcher; 32 import com.android.systemui.plugins.statusbar.StatusBarStateController; 33 import com.android.systemui.statusbar.CommandQueue; 34 import com.android.systemui.statusbar.CrossFadeHelper; 35 import com.android.systemui.statusbar.HeadsUpStatusBarView; 36 import com.android.systemui.statusbar.StatusBarState; 37 import com.android.systemui.statusbar.SysuiStatusBarStateController; 38 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; 39 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 40 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 41 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; 42 import com.android.systemui.statusbar.policy.KeyguardMonitor; 43 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 44 45 import java.util.function.BiConsumer; 46 import java.util.function.Consumer; 47 48 /** 49 * Controls the appearance of heads up notifications in the icon area and the header itself. 50 */ 51 public class HeadsUpAppearanceController implements OnHeadsUpChangedListener, 52 DarkIconDispatcher.DarkReceiver, NotificationWakeUpCoordinator.WakeUpListener { 53 public static final int CONTENT_FADE_DURATION = 110; 54 public static final int CONTENT_FADE_DELAY = 100; 55 private final NotificationIconAreaController mNotificationIconAreaController; 56 private final HeadsUpManagerPhone mHeadsUpManager; 57 private final NotificationStackScrollLayout mStackScroller; 58 private final HeadsUpStatusBarView mHeadsUpStatusBarView; 59 private final View mCenteredIconView; 60 private final View mClockView; 61 private final View mOperatorNameView; 62 private final DarkIconDispatcher mDarkIconDispatcher; 63 private final NotificationPanelView mPanelView; 64 private final Consumer<ExpandableNotificationRow> 65 mSetTrackingHeadsUp = this::setTrackingHeadsUp; 66 private final Runnable mUpdatePanelTranslation = this::updatePanelTranslation; 67 private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction; 68 private final KeyguardBypassController mBypassController; 69 private final StatusBarStateController mStatusBarStateController; 70 private final CommandQueue mCommandQueue; 71 private final NotificationWakeUpCoordinator mWakeUpCoordinator; 72 @VisibleForTesting 73 float mExpandedHeight; 74 @VisibleForTesting 75 boolean mIsExpanded; 76 @VisibleForTesting 77 float mAppearFraction; 78 private ExpandableNotificationRow mTrackedChild; 79 private boolean mShown; 80 private final View.OnLayoutChangeListener mStackScrollLayoutChangeListener = 81 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) 82 -> updatePanelTranslation(); 83 private final ViewClippingUtil.ClippingParameters mParentClippingParams = 84 new ViewClippingUtil.ClippingParameters() { 85 @Override 86 public boolean shouldFinish(View view) { 87 return view.getId() == R.id.status_bar; 88 } 89 }; 90 private boolean mAnimationsEnabled = true; 91 Point mPoint; 92 private KeyguardMonitor mKeyguardMonitor; 93 94 HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, View statusbarView, SysuiStatusBarStateController statusBarStateController, KeyguardBypassController keyguardBypassController, NotificationWakeUpCoordinator wakeUpCoordinator)95 public HeadsUpAppearanceController( 96 NotificationIconAreaController notificationIconAreaController, 97 HeadsUpManagerPhone headsUpManager, 98 View statusbarView, 99 SysuiStatusBarStateController statusBarStateController, 100 KeyguardBypassController keyguardBypassController, 101 NotificationWakeUpCoordinator wakeUpCoordinator) { 102 this(notificationIconAreaController, headsUpManager, statusBarStateController, 103 keyguardBypassController, wakeUpCoordinator, 104 statusbarView.findViewById(R.id.heads_up_status_bar_view), 105 statusbarView.findViewById(R.id.notification_stack_scroller), 106 statusbarView.findViewById(R.id.notification_panel), 107 statusbarView.findViewById(R.id.clock), 108 statusbarView.findViewById(R.id.operator_name_frame), 109 statusbarView.findViewById(R.id.centered_icon_area)); 110 } 111 112 @VisibleForTesting HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, StatusBarStateController stateController, KeyguardBypassController bypassController, NotificationWakeUpCoordinator wakeUpCoordinator, HeadsUpStatusBarView headsUpStatusBarView, NotificationStackScrollLayout stackScroller, NotificationPanelView panelView, View clockView, View operatorNameView, View centeredIconView)113 public HeadsUpAppearanceController( 114 NotificationIconAreaController notificationIconAreaController, 115 HeadsUpManagerPhone headsUpManager, 116 StatusBarStateController stateController, 117 KeyguardBypassController bypassController, 118 NotificationWakeUpCoordinator wakeUpCoordinator, 119 HeadsUpStatusBarView headsUpStatusBarView, 120 NotificationStackScrollLayout stackScroller, 121 NotificationPanelView panelView, 122 View clockView, 123 View operatorNameView, 124 View centeredIconView) { 125 mNotificationIconAreaController = notificationIconAreaController; 126 mHeadsUpManager = headsUpManager; 127 mHeadsUpManager.addListener(this); 128 mHeadsUpStatusBarView = headsUpStatusBarView; 129 mCenteredIconView = centeredIconView; 130 headsUpStatusBarView.setOnDrawingRectChangedListener( 131 () -> updateIsolatedIconLocation(true /* requireUpdate */)); 132 mStackScroller = stackScroller; 133 mPanelView = panelView; 134 panelView.addTrackingHeadsUpListener(mSetTrackingHeadsUp); 135 panelView.addVerticalTranslationListener(mUpdatePanelTranslation); 136 panelView.setHeadsUpAppearanceController(this); 137 mStackScroller.addOnExpandedHeightChangedListener(mSetExpandedHeight); 138 mStackScroller.addOnLayoutChangeListener(mStackScrollLayoutChangeListener); 139 mStackScroller.setHeadsUpAppearanceController(this); 140 mClockView = clockView; 141 mOperatorNameView = operatorNameView; 142 mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class); 143 mDarkIconDispatcher.addDarkReceiver(this); 144 145 mHeadsUpStatusBarView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 146 @Override 147 public void onLayoutChange(View v, int left, int top, int right, int bottom, 148 int oldLeft, int oldTop, int oldRight, int oldBottom) { 149 if (shouldBeVisible()) { 150 updateTopEntry(); 151 152 // trigger scroller to notify the latest panel translation 153 mStackScroller.requestLayout(); 154 } 155 mHeadsUpStatusBarView.removeOnLayoutChangeListener(this); 156 } 157 }); 158 mBypassController = bypassController; 159 mStatusBarStateController = stateController; 160 mWakeUpCoordinator = wakeUpCoordinator; 161 wakeUpCoordinator.addListener(this); 162 mCommandQueue = getComponent(headsUpStatusBarView.getContext(), CommandQueue.class); 163 mKeyguardMonitor = Dependency.get(KeyguardMonitor.class); 164 } 165 166 destroy()167 public void destroy() { 168 mHeadsUpManager.removeListener(this); 169 mHeadsUpStatusBarView.setOnDrawingRectChangedListener(null); 170 mWakeUpCoordinator.removeListener(this); 171 mPanelView.removeTrackingHeadsUpListener(mSetTrackingHeadsUp); 172 mPanelView.removeVerticalTranslationListener(mUpdatePanelTranslation); 173 mPanelView.setHeadsUpAppearanceController(null); 174 mStackScroller.removeOnExpandedHeightChangedListener(mSetExpandedHeight); 175 mStackScroller.removeOnLayoutChangeListener(mStackScrollLayoutChangeListener); 176 mDarkIconDispatcher.removeDarkReceiver(this); 177 } 178 updateIsolatedIconLocation(boolean requireStateUpdate)179 private void updateIsolatedIconLocation(boolean requireStateUpdate) { 180 mNotificationIconAreaController.setIsolatedIconLocation( 181 mHeadsUpStatusBarView.getIconDrawingRect(), requireStateUpdate); 182 } 183 184 @Override onHeadsUpPinned(NotificationEntry entry)185 public void onHeadsUpPinned(NotificationEntry entry) { 186 updateTopEntry(); 187 updateHeader(entry); 188 } 189 190 /** To count the distance from the window right boundary to scroller right boundary. The 191 * distance formula is the following: 192 * Y = screenSize - (SystemWindow's width + Scroller.getRight()) 193 * There are four modes MUST to be considered in Cut Out of RTL. 194 * No Cut Out: 195 * Scroller + NB 196 * NB + Scroller 197 * => SystemWindow = NavigationBar's width 198 * => Y = screenSize - (SystemWindow's width + Scroller.getRight()) 199 * Corner Cut Out or Tall Cut Out: 200 * cut out + Scroller + NB 201 * NB + Scroller + cut out 202 * => SystemWindow = NavigationBar's width 203 * => Y = screenSize - (SystemWindow's width + Scroller.getRight()) 204 * Double Cut Out: 205 * cut out left + Scroller + (NB + cut out right) 206 * SystemWindow = NavigationBar's width + cut out right width 207 * => Y = screenSize - (SystemWindow's width + Scroller.getRight()) 208 * (cut out left + NB) + Scroller + cut out right 209 * SystemWindow = NavigationBar's width + cut out left width 210 * => Y = screenSize - (SystemWindow's width + Scroller.getRight()) 211 * @return the translation X value for RTL. In theory, it should be negative. i.e. -Y 212 */ getRtlTranslation()213 private int getRtlTranslation() { 214 if (mPoint == null) { 215 mPoint = new Point(); 216 } 217 218 int realDisplaySize = 0; 219 if (mStackScroller.getDisplay() != null) { 220 mStackScroller.getDisplay().getRealSize(mPoint); 221 realDisplaySize = mPoint.x; 222 } 223 224 WindowInsets windowInset = mStackScroller.getRootWindowInsets(); 225 DisplayCutout cutout = (windowInset != null) ? windowInset.getDisplayCutout() : null; 226 int sysWinLeft = (windowInset != null) ? windowInset.getStableInsetLeft() : 0; 227 int sysWinRight = (windowInset != null) ? windowInset.getStableInsetRight() : 0; 228 int cutoutLeft = (cutout != null) ? cutout.getSafeInsetLeft() : 0; 229 int cutoutRight = (cutout != null) ? cutout.getSafeInsetRight() : 0; 230 int leftInset = Math.max(sysWinLeft, cutoutLeft); 231 int rightInset = Math.max(sysWinRight, cutoutRight); 232 233 return leftInset + mStackScroller.getRight() + rightInset - realDisplaySize; 234 } 235 updatePanelTranslation()236 public void updatePanelTranslation() { 237 float newTranslation; 238 if (mStackScroller.isLayoutRtl()) { 239 newTranslation = getRtlTranslation(); 240 } else { 241 newTranslation = mStackScroller.getLeft(); 242 } 243 newTranslation += mStackScroller.getTranslationX(); 244 mHeadsUpStatusBarView.setPanelTranslation(newTranslation); 245 } 246 updateTopEntry()247 private void updateTopEntry() { 248 NotificationEntry newEntry = null; 249 if (shouldBeVisible()) { 250 newEntry = mHeadsUpManager.getTopEntry(); 251 } 252 NotificationEntry previousEntry = mHeadsUpStatusBarView.getShowingEntry(); 253 mHeadsUpStatusBarView.setEntry(newEntry); 254 if (newEntry != previousEntry) { 255 boolean animateIsolation = false; 256 if (newEntry == null) { 257 // no heads up anymore, lets start the disappear animation 258 259 setShown(false); 260 animateIsolation = !mIsExpanded; 261 } else if (previousEntry == null) { 262 // We now have a headsUp and didn't have one before. Let's start the disappear 263 // animation 264 setShown(true); 265 animateIsolation = !mIsExpanded; 266 } 267 updateIsolatedIconLocation(false /* requireUpdate */); 268 mNotificationIconAreaController.showIconIsolated(newEntry == null ? null 269 : newEntry.icon, animateIsolation); 270 } 271 } 272 setShown(boolean isShown)273 private void setShown(boolean isShown) { 274 if (mShown != isShown) { 275 mShown = isShown; 276 if (isShown) { 277 updateParentClipping(false /* shouldClip */); 278 mHeadsUpStatusBarView.setVisibility(View.VISIBLE); 279 show(mHeadsUpStatusBarView); 280 hide(mClockView, View.INVISIBLE); 281 if (mCenteredIconView.getVisibility() != View.GONE) { 282 hide(mCenteredIconView, View.INVISIBLE); 283 } 284 if (mOperatorNameView != null) { 285 hide(mOperatorNameView, View.INVISIBLE); 286 } 287 } else { 288 show(mClockView); 289 if (mCenteredIconView.getVisibility() != View.GONE) { 290 show(mCenteredIconView); 291 } 292 if (mOperatorNameView != null) { 293 show(mOperatorNameView); 294 } 295 hide(mHeadsUpStatusBarView, View.GONE, () -> { 296 updateParentClipping(true /* shouldClip */); 297 }); 298 } 299 // Show the status bar icons when the view gets shown / hidden 300 if (mStatusBarStateController.getState() != StatusBarState.SHADE) { 301 mCommandQueue.recomputeDisableFlags( 302 mHeadsUpStatusBarView.getContext().getDisplayId(), false); 303 } 304 } 305 } 306 updateParentClipping(boolean shouldClip)307 private void updateParentClipping(boolean shouldClip) { 308 ViewClippingUtil.setClippingDeactivated( 309 mHeadsUpStatusBarView, !shouldClip, mParentClippingParams); 310 } 311 312 /** 313 * Hides the view and sets the state to endState when finished. 314 * 315 * @param view The view to hide. 316 * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}. 317 * @see HeadsUpAppearanceController#hide(View, int, Runnable) 318 * @see View#setVisibility(int) 319 * 320 */ hide(View view, int endState)321 private void hide(View view, int endState) { 322 hide(view, endState, null); 323 } 324 325 /** 326 * Hides the view and sets the state to endState when finished. 327 * 328 * @param view The view to hide. 329 * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}. 330 * @param callback Runnable to be executed after the view has been hidden. 331 * @see View#setVisibility(int) 332 * 333 */ hide(View view, int endState, Runnable callback)334 private void hide(View view, int endState, Runnable callback) { 335 if (mAnimationsEnabled) { 336 CrossFadeHelper.fadeOut(view, CONTENT_FADE_DURATION /* duration */, 337 0 /* delay */, () -> { 338 view.setVisibility(endState); 339 if (callback != null) { 340 callback.run(); 341 } 342 }); 343 } else { 344 view.setVisibility(endState); 345 if (callback != null) { 346 callback.run(); 347 } 348 } 349 } 350 show(View view)351 private void show(View view) { 352 if (mAnimationsEnabled) { 353 CrossFadeHelper.fadeIn(view, CONTENT_FADE_DURATION /* duration */, 354 CONTENT_FADE_DELAY /* delay */); 355 } else { 356 view.setVisibility(View.VISIBLE); 357 } 358 } 359 360 @VisibleForTesting setAnimationsEnabled(boolean enabled)361 void setAnimationsEnabled(boolean enabled) { 362 mAnimationsEnabled = enabled; 363 } 364 365 @VisibleForTesting isShown()366 public boolean isShown() { 367 return mShown; 368 } 369 370 /** 371 * Should the headsup status bar view be visible right now? This may be different from isShown, 372 * since the headsUp manager might not have notified us yet of the state change. 373 * 374 * @return if the heads up status bar view should be shown 375 */ shouldBeVisible()376 public boolean shouldBeVisible() { 377 boolean notificationsShown = !mWakeUpCoordinator.getNotificationsFullyHidden(); 378 boolean canShow = !mIsExpanded && notificationsShown; 379 if (mBypassController.getBypassEnabled() && 380 (mStatusBarStateController.getState() == StatusBarState.KEYGUARD 381 || mKeyguardMonitor.isKeyguardGoingAway()) 382 && notificationsShown) { 383 canShow = true; 384 } 385 return canShow && mHeadsUpManager.hasPinnedHeadsUp(); 386 } 387 388 @Override onHeadsUpUnPinned(NotificationEntry entry)389 public void onHeadsUpUnPinned(NotificationEntry entry) { 390 updateTopEntry(); 391 updateHeader(entry); 392 } 393 setAppearFraction(float expandedHeight, float appearFraction)394 public void setAppearFraction(float expandedHeight, float appearFraction) { 395 boolean changed = expandedHeight != mExpandedHeight; 396 mExpandedHeight = expandedHeight; 397 mAppearFraction = appearFraction; 398 boolean isExpanded = expandedHeight > 0; 399 // We only notify if the expandedHeight changed and not on the appearFraction, since 400 // otherwise we may run into an infinite loop where the panel and this are constantly 401 // updating themselves over just a small fraction 402 if (changed) { 403 updateHeadsUpHeaders(); 404 } 405 if (isExpanded != mIsExpanded) { 406 mIsExpanded = isExpanded; 407 updateTopEntry(); 408 } 409 } 410 411 /** 412 * Set a headsUp to be tracked, meaning that it is currently being pulled down after being 413 * in a pinned state on the top. The expand animation is different in that case and we need 414 * to update the header constantly afterwards. 415 * 416 * @param trackedChild the tracked headsUp or null if it's not tracking anymore. 417 */ setTrackingHeadsUp(ExpandableNotificationRow trackedChild)418 public void setTrackingHeadsUp(ExpandableNotificationRow trackedChild) { 419 ExpandableNotificationRow previousTracked = mTrackedChild; 420 mTrackedChild = trackedChild; 421 if (previousTracked != null) { 422 updateHeader(previousTracked.getEntry()); 423 } 424 } 425 updateHeadsUpHeaders()426 private void updateHeadsUpHeaders() { 427 mHeadsUpManager.getAllEntries().forEach(entry -> { 428 updateHeader(entry); 429 }); 430 } 431 updateHeader(NotificationEntry entry)432 public void updateHeader(NotificationEntry entry) { 433 ExpandableNotificationRow row = entry.getRow(); 434 float headerVisibleAmount = 1.0f; 435 if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild 436 || row.showingPulsing()) { 437 headerVisibleAmount = mAppearFraction; 438 } 439 row.setHeaderVisibleAmount(headerVisibleAmount); 440 } 441 442 @Override onDarkChanged(Rect area, float darkIntensity, int tint)443 public void onDarkChanged(Rect area, float darkIntensity, int tint) { 444 mHeadsUpStatusBarView.onDarkChanged(area, darkIntensity, tint); 445 } 446 onStateChanged()447 public void onStateChanged() { 448 updateTopEntry(); 449 } 450 readFrom(HeadsUpAppearanceController oldController)451 void readFrom(HeadsUpAppearanceController oldController) { 452 if (oldController != null) { 453 mTrackedChild = oldController.mTrackedChild; 454 mExpandedHeight = oldController.mExpandedHeight; 455 mIsExpanded = oldController.mIsExpanded; 456 mAppearFraction = oldController.mAppearFraction; 457 } 458 } 459 460 @Override onFullyHiddenChanged(boolean isFullyHidden)461 public void onFullyHiddenChanged(boolean isFullyHidden) { 462 updateTopEntry(); 463 } 464 } 465