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 23 import com.android.internal.annotations.VisibleForTesting; 24 import com.android.internal.widget.ViewClippingUtil; 25 import com.android.systemui.Dependency; 26 import com.android.systemui.R; 27 import com.android.systemui.dagger.qualifiers.RootView; 28 import com.android.systemui.plugins.DarkIconDispatcher; 29 import com.android.systemui.plugins.statusbar.StatusBarStateController; 30 import com.android.systemui.statusbar.CommandQueue; 31 import com.android.systemui.statusbar.CrossFadeHelper; 32 import com.android.systemui.statusbar.HeadsUpStatusBarView; 33 import com.android.systemui.statusbar.StatusBarState; 34 import com.android.systemui.statusbar.SysuiStatusBarStateController; 35 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; 36 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 37 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 38 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; 39 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentScope; 40 import com.android.systemui.statusbar.policy.KeyguardStateController; 41 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 42 import com.android.systemui.util.ViewController; 43 44 import java.util.function.BiConsumer; 45 import java.util.function.Consumer; 46 47 import javax.inject.Inject; 48 49 /** 50 * Controls the appearance of heads up notifications in the icon area and the header itself. 51 */ 52 @StatusBarFragmentScope 53 public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBarView> 54 implements OnHeadsUpChangedListener, 55 DarkIconDispatcher.DarkReceiver, 56 NotificationWakeUpCoordinator.WakeUpListener { 57 public static final int CONTENT_FADE_DURATION = 110; 58 public static final int CONTENT_FADE_DELAY = 100; 59 private final NotificationIconAreaController mNotificationIconAreaController; 60 private final HeadsUpManagerPhone mHeadsUpManager; 61 private final NotificationStackScrollLayoutController mStackScrollerController; 62 private final View mCenteredIconView; 63 private final View mClockView; 64 private final View mOperatorNameView; 65 private final DarkIconDispatcher mDarkIconDispatcher; 66 private final NotificationPanelViewController mNotificationPanelViewController; 67 private final Consumer<ExpandableNotificationRow> 68 mSetTrackingHeadsUp = this::setTrackingHeadsUp; 69 private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction; 70 private final KeyguardBypassController mBypassController; 71 private final StatusBarStateController mStatusBarStateController; 72 private final CommandQueue mCommandQueue; 73 private final NotificationWakeUpCoordinator mWakeUpCoordinator; 74 @VisibleForTesting 75 float mExpandedHeight; 76 @VisibleForTesting 77 float mAppearFraction; 78 private ExpandableNotificationRow mTrackedChild; 79 private boolean mShown; 80 private final ViewClippingUtil.ClippingParameters mParentClippingParams = 81 new ViewClippingUtil.ClippingParameters() { 82 @Override 83 public boolean shouldFinish(View view) { 84 return view.getId() == R.id.status_bar; 85 } 86 }; 87 private boolean mAnimationsEnabled = true; 88 Point mPoint; 89 private KeyguardStateController mKeyguardStateController; 90 91 @Inject HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, NotificationStackScrollLayoutController notificationStackScrollLayoutController, SysuiStatusBarStateController statusBarStateController, KeyguardBypassController keyguardBypassController, KeyguardStateController keyguardStateController, NotificationWakeUpCoordinator wakeUpCoordinator, CommandQueue commandQueue, NotificationPanelViewController notificationPanelViewController, @RootView PhoneStatusBarView statusBarView)92 public HeadsUpAppearanceController( 93 NotificationIconAreaController notificationIconAreaController, 94 HeadsUpManagerPhone headsUpManager, 95 NotificationStackScrollLayoutController notificationStackScrollLayoutController, 96 SysuiStatusBarStateController statusBarStateController, 97 KeyguardBypassController keyguardBypassController, 98 KeyguardStateController keyguardStateController, 99 NotificationWakeUpCoordinator wakeUpCoordinator, CommandQueue commandQueue, 100 NotificationPanelViewController notificationPanelViewController, 101 @RootView PhoneStatusBarView statusBarView) { 102 this(notificationIconAreaController, headsUpManager, statusBarStateController, 103 keyguardBypassController, wakeUpCoordinator, keyguardStateController, 104 commandQueue, notificationStackScrollLayoutController, 105 notificationPanelViewController, 106 // TODO(b/205609837): We should have the StatusBarFragmentComponent provide these 107 // four views, and then we can delete this constructor and just use the one below 108 // (which also removes the undesirable @VisibleForTesting). 109 statusBarView.findViewById(R.id.heads_up_status_bar_view), 110 statusBarView.findViewById(R.id.clock), 111 statusBarView.findViewById(R.id.operator_name_frame), 112 statusBarView.findViewById(R.id.centered_icon_area)); 113 } 114 115 @VisibleForTesting HeadsUpAppearanceController( NotificationIconAreaController notificationIconAreaController, HeadsUpManagerPhone headsUpManager, StatusBarStateController stateController, KeyguardBypassController bypassController, NotificationWakeUpCoordinator wakeUpCoordinator, KeyguardStateController keyguardStateController, CommandQueue commandQueue, NotificationStackScrollLayoutController stackScrollerController, NotificationPanelViewController notificationPanelViewController, HeadsUpStatusBarView headsUpStatusBarView, View clockView, View operatorNameView, View centeredIconView)116 public HeadsUpAppearanceController( 117 NotificationIconAreaController notificationIconAreaController, 118 HeadsUpManagerPhone headsUpManager, 119 StatusBarStateController stateController, 120 KeyguardBypassController bypassController, 121 NotificationWakeUpCoordinator wakeUpCoordinator, 122 KeyguardStateController keyguardStateController, 123 CommandQueue commandQueue, 124 NotificationStackScrollLayoutController stackScrollerController, 125 NotificationPanelViewController notificationPanelViewController, 126 HeadsUpStatusBarView headsUpStatusBarView, 127 View clockView, 128 View operatorNameView, 129 View centeredIconView) { 130 super(headsUpStatusBarView); 131 mNotificationIconAreaController = notificationIconAreaController; 132 mHeadsUpManager = headsUpManager; 133 mCenteredIconView = centeredIconView; 134 135 // We may be mid-HUN-expansion when this controller is re-created (for example, if the user 136 // has started pulling down the notification shade from the HUN and then the font size 137 // changes). We need to re-fetch these values since they're used to correctly display the 138 // HUN during this shade expansion. 139 mTrackedChild = notificationPanelViewController.getTrackedHeadsUpNotification(); 140 mAppearFraction = stackScrollerController.getAppearFraction(); 141 mExpandedHeight = stackScrollerController.getExpandedHeight(); 142 143 mStackScrollerController = stackScrollerController; 144 mNotificationPanelViewController = notificationPanelViewController; 145 mStackScrollerController.setHeadsUpAppearanceController(this); 146 mClockView = clockView; 147 mOperatorNameView = operatorNameView; 148 mDarkIconDispatcher = Dependency.get(DarkIconDispatcher.class); 149 150 mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 151 @Override 152 public void onLayoutChange(View v, int left, int top, int right, int bottom, 153 int oldLeft, int oldTop, int oldRight, int oldBottom) { 154 if (shouldBeVisible()) { 155 updateTopEntry(); 156 157 // trigger scroller to notify the latest panel translation 158 mStackScrollerController.requestLayout(); 159 } 160 mView.removeOnLayoutChangeListener(this); 161 } 162 }); 163 mBypassController = bypassController; 164 mStatusBarStateController = stateController; 165 mWakeUpCoordinator = wakeUpCoordinator; 166 mCommandQueue = commandQueue; 167 mKeyguardStateController = keyguardStateController; 168 } 169 170 @Override onViewAttached()171 protected void onViewAttached() { 172 mHeadsUpManager.addListener(this); 173 mView.setOnDrawingRectChangedListener( 174 () -> updateIsolatedIconLocation(true /* requireUpdate */)); 175 mWakeUpCoordinator.addListener(this); 176 mNotificationPanelViewController.addTrackingHeadsUpListener(mSetTrackingHeadsUp); 177 mNotificationPanelViewController.setHeadsUpAppearanceController(this); 178 mStackScrollerController.addOnExpandedHeightChangedListener(mSetExpandedHeight); 179 mDarkIconDispatcher.addDarkReceiver(this); 180 } 181 182 @Override onViewDetached()183 protected void onViewDetached() { 184 mHeadsUpManager.removeListener(this); 185 mView.setOnDrawingRectChangedListener(null); 186 mWakeUpCoordinator.removeListener(this); 187 mNotificationPanelViewController.removeTrackingHeadsUpListener(mSetTrackingHeadsUp); 188 mNotificationPanelViewController.setHeadsUpAppearanceController(null); 189 mStackScrollerController.removeOnExpandedHeightChangedListener(mSetExpandedHeight); 190 mDarkIconDispatcher.removeDarkReceiver(this); 191 } 192 updateIsolatedIconLocation(boolean requireStateUpdate)193 private void updateIsolatedIconLocation(boolean requireStateUpdate) { 194 mNotificationIconAreaController.setIsolatedIconLocation( 195 mView.getIconDrawingRect(), requireStateUpdate); 196 } 197 198 @Override onHeadsUpPinned(NotificationEntry entry)199 public void onHeadsUpPinned(NotificationEntry entry) { 200 updateTopEntry(); 201 updateHeader(entry); 202 } 203 updateTopEntry()204 private void updateTopEntry() { 205 NotificationEntry newEntry = null; 206 if (shouldBeVisible()) { 207 newEntry = mHeadsUpManager.getTopEntry(); 208 } 209 NotificationEntry previousEntry = mView.getShowingEntry(); 210 mView.setEntry(newEntry); 211 if (newEntry != previousEntry) { 212 boolean animateIsolation = false; 213 if (newEntry == null) { 214 // no heads up anymore, lets start the disappear animation 215 216 setShown(false); 217 animateIsolation = !isExpanded(); 218 } else if (previousEntry == null) { 219 // We now have a headsUp and didn't have one before. Let's start the disappear 220 // animation 221 setShown(true); 222 animateIsolation = !isExpanded(); 223 } 224 updateIsolatedIconLocation(false /* requireUpdate */); 225 mNotificationIconAreaController.showIconIsolated(newEntry == null ? null 226 : newEntry.getIcons().getStatusBarIcon(), animateIsolation); 227 } 228 } 229 setShown(boolean isShown)230 private void setShown(boolean isShown) { 231 if (mShown != isShown) { 232 mShown = isShown; 233 if (isShown) { 234 updateParentClipping(false /* shouldClip */); 235 mView.setVisibility(View.VISIBLE); 236 show(mView); 237 hide(mClockView, View.INVISIBLE); 238 if (mCenteredIconView.getVisibility() != View.GONE) { 239 hide(mCenteredIconView, View.INVISIBLE); 240 } 241 if (mOperatorNameView != null) { 242 hide(mOperatorNameView, View.INVISIBLE); 243 } 244 } else { 245 show(mClockView); 246 if (mCenteredIconView.getVisibility() != View.GONE) { 247 show(mCenteredIconView); 248 } 249 if (mOperatorNameView != null) { 250 show(mOperatorNameView); 251 } 252 hide(mView, View.GONE, () -> { 253 updateParentClipping(true /* shouldClip */); 254 }); 255 } 256 // Show the status bar icons when the view gets shown / hidden 257 if (mStatusBarStateController.getState() != StatusBarState.SHADE) { 258 mCommandQueue.recomputeDisableFlags( 259 mView.getContext().getDisplayId(), false); 260 } 261 } 262 } 263 updateParentClipping(boolean shouldClip)264 private void updateParentClipping(boolean shouldClip) { 265 ViewClippingUtil.setClippingDeactivated( 266 mView, !shouldClip, mParentClippingParams); 267 } 268 269 /** 270 * Hides the view and sets the state to endState when finished. 271 * 272 * @param view The view to hide. 273 * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}. 274 * @see HeadsUpAppearanceController#hide(View, int, Runnable) 275 * @see View#setVisibility(int) 276 * 277 */ hide(View view, int endState)278 private void hide(View view, int endState) { 279 hide(view, endState, null); 280 } 281 282 /** 283 * Hides the view and sets the state to endState when finished. 284 * 285 * @param view The view to hide. 286 * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}. 287 * @param callback Runnable to be executed after the view has been hidden. 288 * @see View#setVisibility(int) 289 * 290 */ hide(View view, int endState, Runnable callback)291 private void hide(View view, int endState, Runnable callback) { 292 if (mAnimationsEnabled) { 293 CrossFadeHelper.fadeOut(view, CONTENT_FADE_DURATION /* duration */, 294 0 /* delay */, () -> { 295 view.setVisibility(endState); 296 if (callback != null) { 297 callback.run(); 298 } 299 }); 300 } else { 301 view.setVisibility(endState); 302 if (callback != null) { 303 callback.run(); 304 } 305 } 306 } 307 show(View view)308 private void show(View view) { 309 if (mAnimationsEnabled) { 310 CrossFadeHelper.fadeIn(view, CONTENT_FADE_DURATION /* duration */, 311 CONTENT_FADE_DELAY /* delay */); 312 } else { 313 view.setVisibility(View.VISIBLE); 314 } 315 } 316 317 @VisibleForTesting setAnimationsEnabled(boolean enabled)318 void setAnimationsEnabled(boolean enabled) { 319 mAnimationsEnabled = enabled; 320 } 321 322 @VisibleForTesting isShown()323 public boolean isShown() { 324 return mShown; 325 } 326 327 /** 328 * Should the headsup status bar view be visible right now? This may be different from isShown, 329 * since the headsUp manager might not have notified us yet of the state change. 330 * 331 * @return if the heads up status bar view should be shown 332 */ shouldBeVisible()333 public boolean shouldBeVisible() { 334 boolean notificationsShown = !mWakeUpCoordinator.getNotificationsFullyHidden(); 335 boolean canShow = !isExpanded() && notificationsShown; 336 if (mBypassController.getBypassEnabled() && 337 (mStatusBarStateController.getState() == StatusBarState.KEYGUARD 338 || mKeyguardStateController.isKeyguardGoingAway()) 339 && notificationsShown) { 340 canShow = true; 341 } 342 return canShow && mHeadsUpManager.hasPinnedHeadsUp(); 343 } 344 345 @Override onHeadsUpUnPinned(NotificationEntry entry)346 public void onHeadsUpUnPinned(NotificationEntry entry) { 347 updateTopEntry(); 348 updateHeader(entry); 349 } 350 setAppearFraction(float expandedHeight, float appearFraction)351 public void setAppearFraction(float expandedHeight, float appearFraction) { 352 boolean changed = expandedHeight != mExpandedHeight; 353 boolean oldIsExpanded = isExpanded(); 354 355 mExpandedHeight = expandedHeight; 356 mAppearFraction = appearFraction; 357 // We only notify if the expandedHeight changed and not on the appearFraction, since 358 // otherwise we may run into an infinite loop where the panel and this are constantly 359 // updating themselves over just a small fraction 360 if (changed) { 361 updateHeadsUpHeaders(); 362 } 363 if (isExpanded() != oldIsExpanded) { 364 updateTopEntry(); 365 } 366 } 367 368 /** 369 * Set a headsUp to be tracked, meaning that it is currently being pulled down after being 370 * in a pinned state on the top. The expand animation is different in that case and we need 371 * to update the header constantly afterwards. 372 * 373 * @param trackedChild the tracked headsUp or null if it's not tracking anymore. 374 */ setTrackingHeadsUp(ExpandableNotificationRow trackedChild)375 public void setTrackingHeadsUp(ExpandableNotificationRow trackedChild) { 376 ExpandableNotificationRow previousTracked = mTrackedChild; 377 mTrackedChild = trackedChild; 378 if (previousTracked != null) { 379 updateHeader(previousTracked.getEntry()); 380 } 381 } 382 isExpanded()383 private boolean isExpanded() { 384 return mExpandedHeight > 0; 385 } 386 updateHeadsUpHeaders()387 private void updateHeadsUpHeaders() { 388 mHeadsUpManager.getAllEntries().forEach(entry -> { 389 updateHeader(entry); 390 }); 391 } 392 updateHeader(NotificationEntry entry)393 public void updateHeader(NotificationEntry entry) { 394 ExpandableNotificationRow row = entry.getRow(); 395 float headerVisibleAmount = 1.0f; 396 if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild 397 || row.showingPulsing()) { 398 headerVisibleAmount = mAppearFraction; 399 } 400 row.setHeaderVisibleAmount(headerVisibleAmount); 401 } 402 403 @Override onDarkChanged(Rect area, float darkIntensity, int tint)404 public void onDarkChanged(Rect area, float darkIntensity, int tint) { 405 mView.onDarkChanged(area, darkIntensity, tint); 406 } 407 onStateChanged()408 public void onStateChanged() { 409 updateTopEntry(); 410 } 411 412 @Override onFullyHiddenChanged(boolean isFullyHidden)413 public void onFullyHiddenChanged(boolean isFullyHidden) { 414 updateTopEntry(); 415 } 416 } 417