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