1 /* 2 * Copyright (C) 2014 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.Flags.centralizedStatusBarHeightFix; 20 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; 21 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInScale; 22 import static com.android.systemui.statusbar.notification.NotificationUtils.interpolate; 23 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.util.MathUtils; 27 28 import com.android.app.animation.Interpolators; 29 import com.android.keyguard.BouncerPanelExpansionCalculator; 30 import com.android.keyguard.KeyguardStatusView; 31 import com.android.systemui.log.LogBuffer; 32 import com.android.systemui.log.core.Logger; 33 import com.android.systemui.log.dagger.KeyguardClockLog; 34 import com.android.systemui.res.R; 35 import com.android.systemui.shade.LargeScreenHeaderHelper; 36 import com.android.systemui.shade.ShadeViewController; 37 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherListView; 38 39 import javax.inject.Inject; 40 41 /** 42 * Utility class to calculate the clock position and top padding of notifications on Keyguard. 43 */ 44 public class KeyguardClockPositionAlgorithm { 45 private static final String TAG = "KeyguardClockPositionAlgorithm"; 46 private static final boolean DEBUG = false; 47 48 /** 49 * Margin between the bottom of the status view and the notification shade. 50 */ 51 private int mStatusViewBottomMargin; 52 53 /** 54 * Height of {@link KeyguardStatusView}. 55 */ 56 private int mKeyguardStatusHeight; 57 58 /** 59 * Height of user avatar used by the multi-user switcher. This could either be the 60 * {@link KeyguardUserSwitcherListView} when it is closed and only the current user's icon is 61 * visible, or it could be height of the avatar used by the 62 * {@link com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController}. 63 */ 64 private int mUserSwitchHeight; 65 66 /** 67 * Preferred Y position of user avatar used by the multi-user switcher. 68 */ 69 private int mUserSwitchPreferredY; 70 71 /** 72 * Minimum top margin to avoid overlap with status bar or multi-user switcher avatar. 73 */ 74 private int mMinTopMargin; 75 76 /** 77 * Minimum top inset (in pixels) to avoid overlap with any display cutouts. 78 */ 79 private int mCutoutTopInset = 0; 80 81 /** 82 * Recommended distance from the status bar. 83 */ 84 private int mContainerTopPadding; 85 86 /** 87 * Top margin of notifications introduced by presence of split shade header / status bar 88 */ 89 private int mSplitShadeTopNotificationsMargin; 90 91 /** 92 * Target margin for notifications and clock from the top of the screen in split shade 93 */ 94 private int mSplitShadeTargetTopMargin; 95 96 /** 97 * @see ShadeViewController#getExpandedFraction() 98 */ 99 private float mPanelExpansion; 100 101 /** 102 * Max burn-in prevention x translation. 103 */ 104 private int mMaxBurnInPreventionOffsetX; 105 106 /** 107 * Max burn-in prevention y translation for clock layouts. 108 */ 109 private int mMaxBurnInPreventionOffsetYClock; 110 111 /** 112 * Current burn-in prevention y translation. 113 */ 114 private float mCurrentBurnInOffsetY; 115 116 /** 117 * Doze/AOD transition amount. 118 */ 119 private float mDarkAmount; 120 121 /** 122 * How visible the quick settings panel is. 123 */ 124 private float mQsExpansion; 125 126 private float mOverStretchAmount; 127 128 /** 129 * Setting if bypass is enabled. If true the clock should always be positioned like it's dark 130 * and other minor adjustments. 131 */ 132 private boolean mBypassEnabled; 133 134 /** 135 * The stackscroller padding when unlocked 136 */ 137 private int mUnlockedStackScrollerPadding; 138 139 private boolean mIsSplitShade; 140 141 /** 142 * Top location of the udfps icon. This includes the worst case (highest) burn-in 143 * offset that would make the top physically highest on the screen. 144 * 145 * Set to -1 if udfps is not enrolled on the device. 146 */ 147 private float mUdfpsTop; 148 149 /** 150 * Bottom y-position of the currently visible clock 151 */ 152 private float mClockBottom; 153 154 /** 155 * If true, try to keep clock aligned to the top of the display. Else, assume the clock 156 * is center aligned. 157 */ 158 private boolean mIsClockTopAligned; 159 160 private Logger mLogger; 161 162 @Inject KeyguardClockPositionAlgorithm(@eyguardClockLog LogBuffer logBuffer)163 public KeyguardClockPositionAlgorithm(@KeyguardClockLog LogBuffer logBuffer) { 164 mLogger = new Logger(logBuffer, TAG); 165 } 166 167 /** Refreshes the dimension values. */ loadDimens(Context context, Resources res)168 public void loadDimens(Context context, Resources res) { 169 mStatusViewBottomMargin = 170 res.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin); 171 mSplitShadeTopNotificationsMargin = 172 centralizedStatusBarHeightFix() 173 ? LargeScreenHeaderHelper.getLargeScreenHeaderHeight(context) 174 : res.getDimensionPixelSize(R.dimen.large_screen_shade_header_height); 175 mSplitShadeTargetTopMargin = 176 res.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin); 177 178 mContainerTopPadding = 179 res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin); 180 mMaxBurnInPreventionOffsetX = res.getDimensionPixelSize( 181 R.dimen.burn_in_prevention_offset_x); 182 mMaxBurnInPreventionOffsetYClock = res.getDimensionPixelSize( 183 R.dimen.burn_in_prevention_offset_y_clock); 184 } 185 186 /** 187 * Sets up algorithm values. 188 */ setup(int keyguardStatusBarHeaderHeight, float panelExpansion, int keyguardStatusHeight, int userSwitchHeight, int userSwitchPreferredY, float dark, float overStretchAmount, boolean bypassEnabled, int unlockedStackScrollerPadding, float qsExpansion, int cutoutTopInset, boolean isSplitShade, float udfpsTop, float clockBottom, boolean isClockTopAligned)189 public void setup(int keyguardStatusBarHeaderHeight, float panelExpansion, 190 int keyguardStatusHeight, int userSwitchHeight, int userSwitchPreferredY, 191 float dark, float overStretchAmount, boolean bypassEnabled, 192 int unlockedStackScrollerPadding, float qsExpansion, int cutoutTopInset, 193 boolean isSplitShade, float udfpsTop, float clockBottom, boolean isClockTopAligned) { 194 mMinTopMargin = keyguardStatusBarHeaderHeight + Math.max(mContainerTopPadding, 195 userSwitchHeight); 196 mPanelExpansion = BouncerPanelExpansionCalculator 197 .getKeyguardClockScaledExpansion(panelExpansion); 198 mKeyguardStatusHeight = keyguardStatusHeight + mStatusViewBottomMargin; 199 mUserSwitchHeight = userSwitchHeight; 200 mUserSwitchPreferredY = userSwitchPreferredY; 201 mDarkAmount = dark; 202 mOverStretchAmount = overStretchAmount; 203 mBypassEnabled = bypassEnabled; 204 mUnlockedStackScrollerPadding = unlockedStackScrollerPadding; 205 mQsExpansion = qsExpansion; 206 mCutoutTopInset = cutoutTopInset; 207 mIsSplitShade = isSplitShade; 208 mUdfpsTop = udfpsTop; 209 mClockBottom = clockBottom; 210 mIsClockTopAligned = isClockTopAligned; 211 } 212 run(Result result)213 public void run(Result result) { 214 final int y = getClockY(mPanelExpansion, mDarkAmount); 215 result.clockY = y; 216 result.userSwitchY = getUserSwitcherY(mPanelExpansion); 217 result.clockYFullyDozing = getClockY( 218 1.0f /* panelExpansion */, 1.0f /* darkAmount */); 219 result.clockAlpha = getClockAlpha(y); 220 result.stackScrollerPadding = getStackScrollerPadding(y); 221 result.stackScrollerPaddingExpanded = getStackScrollerPaddingExpanded(); 222 result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount); 223 result.clockScale = interpolate(getBurnInScale(), 1.0f, 1.0f - mDarkAmount); 224 } 225 getStackScrollerPaddingExpanded()226 private int getStackScrollerPaddingExpanded() { 227 if (mBypassEnabled) { 228 return mUnlockedStackScrollerPadding; 229 } else if (mIsSplitShade) { 230 return getClockY(1.0f, mDarkAmount) + mUserSwitchHeight; 231 } else { 232 return getClockY(1.0f, mDarkAmount) + mKeyguardStatusHeight; 233 } 234 } 235 getStackScrollerPadding(int clockYPosition)236 private int getStackScrollerPadding(int clockYPosition) { 237 if (mBypassEnabled) { 238 return (int) (mUnlockedStackScrollerPadding + mOverStretchAmount); 239 } else if (mIsSplitShade) { 240 // mCurrentBurnInOffsetY is subtracted to make notifications not follow clock adjustment 241 // for burn-in. It can make pulsing notification go too high and it will get clipped 242 return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight 243 - (int) mCurrentBurnInOffsetY; 244 } else { 245 return clockYPosition + mKeyguardStatusHeight; 246 } 247 } 248 249 /** 250 * @param nsslTop NotificationStackScrollLayout top, which is below top of the srceen. 251 * @return Distance from nsslTop to top of the first view in the lockscreen shade. 252 */ getLockscreenNotifPadding(float nsslTop)253 public float getLockscreenNotifPadding(float nsslTop) { 254 if (mBypassEnabled) { 255 return mUnlockedStackScrollerPadding - nsslTop; 256 } else if (mIsSplitShade) { 257 return mSplitShadeTargetTopMargin + mUserSwitchHeight - nsslTop; 258 } else { 259 // Non-bypass portrait shade already uses values from nsslTop 260 // so we don't need to subtract it here. 261 return mMinTopMargin + mKeyguardStatusHeight; 262 } 263 } 264 265 /** 266 * give the static topMargin, used for lockscreen clocks to get the initial translationY 267 * to do counter translation 268 */ getExpandedPreferredClockY()269 public int getExpandedPreferredClockY() { 270 if (mIsSplitShade) { 271 return mSplitShadeTargetTopMargin; 272 } else { 273 return mMinTopMargin; 274 } 275 } 276 getLockscreenStatusViewHeight()277 public int getLockscreenStatusViewHeight() { 278 return mKeyguardStatusHeight; 279 } 280 getClockY(float panelExpansion, float darkAmount)281 private int getClockY(float panelExpansion, float darkAmount) { 282 float clockYRegular = getExpandedPreferredClockY(); 283 284 // Dividing the height creates a smoother transition when the user swipes up to unlock 285 float clockYBouncer = -mKeyguardStatusHeight / 3.0f; 286 287 // Move clock up while collapsing the shade 288 float shadeExpansion = Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(panelExpansion); 289 float clockY = MathUtils.lerp(clockYBouncer, clockYRegular, shadeExpansion); 290 291 // This will keep the clock at the top but out of the cutout area 292 float shift = 0; 293 if (clockY - mMaxBurnInPreventionOffsetYClock < mCutoutTopInset) { 294 shift = mCutoutTopInset - (clockY - mMaxBurnInPreventionOffsetYClock); 295 } 296 297 int burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock; // requested offset 298 final boolean hasUdfps = mUdfpsTop > -1; 299 if (hasUdfps && !mIsClockTopAligned) { 300 // ensure clock doesn't overlap with the udfps icon 301 if (mUdfpsTop < mClockBottom) { 302 // sometimes the clock textView extends beyond udfps, so let's just use the 303 // space above the KeyguardStatusView/clock as our burn-in offset 304 burnInPreventionOffsetY = (int) (clockY - mCutoutTopInset) / 2; 305 if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) { 306 burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock; 307 } 308 shift = -burnInPreventionOffsetY; 309 } else { 310 float upperSpace = clockY - mCutoutTopInset; 311 float lowerSpace = mUdfpsTop - mClockBottom; 312 // center the burn-in offset within the upper + lower space 313 burnInPreventionOffsetY = (int) (lowerSpace + upperSpace) / 2; 314 if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) { 315 burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock; 316 } 317 shift = (lowerSpace - upperSpace) / 2; 318 } 319 } 320 321 float fullyDarkBurnInOffset = burnInPreventionOffsetY(burnInPreventionOffsetY); 322 float clockYDark = clockY + fullyDarkBurnInOffset + shift; 323 mCurrentBurnInOffsetY = MathUtils.lerp(0, fullyDarkBurnInOffset, darkAmount); 324 325 if (DEBUG) { 326 final float finalShift = shift; 327 final float finalBurnInPreventionOffsetY = burnInPreventionOffsetY; 328 mLogger.i(msg -> { 329 final String inputs = "panelExpansion: " + panelExpansion 330 + " darkAmount: " + darkAmount; 331 final String outputs = "clockY: " + clockY 332 + " burnInPreventionOffsetY: " + finalBurnInPreventionOffsetY 333 + " fullyDarkBurnInOffset: " + fullyDarkBurnInOffset 334 + " shift: " + finalShift 335 + " mOverStretchAmount: " + mOverStretchAmount 336 + " mCurrentBurnInOffsetY: " + mCurrentBurnInOffsetY; 337 return inputs + " -> " + outputs; 338 }, msg -> { 339 return kotlin.Unit.INSTANCE; 340 }); 341 } 342 return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount); 343 } 344 getUserSwitcherY(float panelExpansion)345 private int getUserSwitcherY(float panelExpansion) { 346 float userSwitchYRegular = mUserSwitchPreferredY; 347 float userSwitchYBouncer = -mKeyguardStatusHeight - mUserSwitchHeight; 348 349 // Move user-switch up while collapsing the shade 350 float shadeExpansion = Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(panelExpansion); 351 float userSwitchY = MathUtils.lerp(userSwitchYBouncer, userSwitchYRegular, shadeExpansion); 352 353 return (int) (userSwitchY + mOverStretchAmount); 354 } 355 356 /** 357 * We might want to fade out the clock when the user is swiping up. 358 * One exception is when the bouncer will become visible, in this cause the clock 359 * should always persist. 360 * 361 * @param y Current clock Y. 362 * @return Alpha from 0 to 1. 363 */ getClockAlpha(int y)364 private float getClockAlpha(int y) { 365 float alphaKeyguard = Math.max(0, y / Math.max(1f, getClockY(1f, mDarkAmount))); 366 if (!mIsSplitShade) { 367 // in split shade QS are always expanded so this factor shouldn't apply 368 float qsAlphaFactor = MathUtils.saturate(mQsExpansion / 0.3f); 369 qsAlphaFactor = 1f - qsAlphaFactor; 370 alphaKeyguard *= qsAlphaFactor; 371 } 372 alphaKeyguard = Interpolators.ACCELERATE.getInterpolation(alphaKeyguard); 373 return MathUtils.lerp(alphaKeyguard, 1f, mDarkAmount); 374 } 375 burnInPreventionOffsetY(int offset)376 private float burnInPreventionOffsetY(int offset) { 377 return getBurnInOffset(offset * 2, false /* xAxis */) - offset; 378 } 379 burnInPreventionOffsetX()380 private float burnInPreventionOffsetX() { 381 return getBurnInOffset(mMaxBurnInPreventionOffsetX, true /* xAxis */); 382 } 383 384 public static class Result { 385 386 /** 387 * The x translation of the clock. 388 */ 389 public int clockX; 390 391 /** 392 * The y translation of the clock. 393 */ 394 public int clockY; 395 396 /** 397 * The y translation of the multi-user switch. 398 */ 399 public int userSwitchY; 400 401 /** 402 * The y translation of the clock when we're fully dozing. 403 */ 404 public int clockYFullyDozing; 405 406 /** 407 * The alpha value of the clock. 408 */ 409 public float clockAlpha; 410 411 /** 412 * Amount to scale the large clock (0.0 - 1.0) 413 */ 414 public float clockScale; 415 416 /** 417 * The top padding of the stack scroller, in pixels. 418 */ 419 public int stackScrollerPadding; 420 421 /** 422 * The top padding of the stack scroller, in pixels when fully expanded. 423 */ 424 public int stackScrollerPaddingExpanded; 425 } 426 } 427