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