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