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
17package com.android.systemui.statusbar.phone;
18
19import static com.android.systemui.statusbar.notification.NotificationUtils.interpolate;
20
21import android.content.res.Resources;
22import android.util.MathUtils;
23
24import com.android.keyguard.KeyguardStatusView;
25import com.android.systemui.Interpolators;
26import com.android.systemui.R;
27
28/**
29 * Utility class to calculate the clock position and top padding of notifications on Keyguard.
30 */
31public class KeyguardClockPositionAlgorithm {
32
33    private static final long MILLIS_PER_MINUTES = 1000 * 60;
34    private static final float BURN_IN_PREVENTION_PERIOD_Y = 521;
35    private static final float BURN_IN_PREVENTION_PERIOD_X = 83;
36
37    /**
38     * How much the clock height influences the shade position.
39     * 0 means nothing, 1 means move the shade up by the height of the clock
40     * 0.5f means move the shade up by half of the size of the clock.
41     */
42    private static float CLOCK_HEIGHT_WEIGHT = 0.7f;
43
44    /**
45     * Margin between the bottom of the clock and the notification shade.
46     */
47    private int mClockNotificationsMargin;
48
49    /**
50     * Height of the parent view - display size in px.
51     */
52    private int mHeight;
53
54    /**
55     * Height of {@link KeyguardStatusView}.
56     */
57    private int mKeyguardStatusHeight;
58
59    /**
60     * Height of notification stack: Sum of height of each notification.
61     */
62    private int mNotificationStackHeight;
63
64    /**
65     * Minimum top margin to avoid overlap with status bar.
66     */
67    private int mMinTopMargin;
68
69    /**
70     * Maximum bottom padding to avoid overlap with {@link KeyguardBottomAreaView} or
71     * the ambient indication.
72     */
73    private int mMaxShadeBottom;
74
75    /**
76     * Minimum distance from the status bar.
77     */
78    private int mContainerTopPadding;
79
80    /**
81     * @see NotificationPanelView#getExpandedFraction()
82     */
83    private float mPanelExpansion;
84
85    /**
86     * Burn-in prevention x translation.
87     */
88    private int mBurnInPreventionOffsetX;
89
90    /**
91     * Burn-in prevention y translation.
92     */
93    private int mBurnInPreventionOffsetY;
94
95    /**
96     * Clock vertical padding when pulsing.
97     */
98    private int mPulsingPadding;
99
100    /**
101     * Doze/AOD transition amount.
102     */
103    private float mDarkAmount;
104
105    /**
106     * If keyguard will require a password or just fade away.
107     */
108    private boolean mCurrentlySecure;
109
110    /**
111     * Dozing and receiving a notification (AOD notification.)
112     */
113    private boolean mPulsing;
114
115    /**
116     * Distance in pixels between the top of the screen and the first view of the bouncer.
117     */
118    private int mBouncerTop;
119
120    /**
121     * Refreshes the dimension values.
122     */
123    public void loadDimens(Resources res) {
124        mClockNotificationsMargin = res.getDimensionPixelSize(
125                R.dimen.keyguard_clock_notifications_margin);
126        mContainerTopPadding = res.getDimensionPixelSize(
127                R.dimen.keyguard_clock_top_margin);
128        mBurnInPreventionOffsetX = res.getDimensionPixelSize(
129                R.dimen.burn_in_prevention_offset_x);
130        mBurnInPreventionOffsetY = res.getDimensionPixelSize(
131                R.dimen.burn_in_prevention_offset_y);
132        mPulsingPadding = res.getDimensionPixelSize(
133                R.dimen.widget_pulsing_bottom_padding);
134    }
135
136    public void setup(int minTopMargin, int maxShadeBottom, int notificationStackHeight,
137            float panelExpansion, int parentHeight,
138            int keyguardStatusHeight, float dark, boolean secure, boolean pulsing,
139            int bouncerTop) {
140        mMinTopMargin = minTopMargin + mContainerTopPadding;
141        mMaxShadeBottom = maxShadeBottom;
142        mNotificationStackHeight = notificationStackHeight;
143        mPanelExpansion = panelExpansion;
144        mHeight = parentHeight;
145        mKeyguardStatusHeight = keyguardStatusHeight;
146        mDarkAmount = dark;
147        mCurrentlySecure = secure;
148        mPulsing = pulsing;
149        mBouncerTop = bouncerTop;
150    }
151
152    public void run(Result result) {
153        final int y = getClockY();
154        result.clockY = y;
155        result.clockAlpha = getClockAlpha(y);
156        result.stackScrollerPadding = y + (mPulsing ? 0 : mKeyguardStatusHeight);
157        result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount);
158    }
159
160    public float getMinStackScrollerPadding() {
161        return mMinTopMargin + mKeyguardStatusHeight + mClockNotificationsMargin;
162    }
163
164    private int getMaxClockY() {
165        return mHeight / 2 - mKeyguardStatusHeight - mClockNotificationsMargin;
166    }
167
168    /**
169     * Vertically align the clock and the shade in the available space considering only
170     * a percentage of the clock height defined by {@code CLOCK_HEIGHT_WEIGHT}.
171     * @return Clock Y in pixels.
172     */
173    public int getExpandedClockPosition() {
174        final int availableHeight = mMaxShadeBottom - mMinTopMargin;
175        final int containerCenter = mMinTopMargin + availableHeight / 2;
176
177        float y = containerCenter - mKeyguardStatusHeight * CLOCK_HEIGHT_WEIGHT
178                - mClockNotificationsMargin - mNotificationStackHeight / 2;
179        if (y < mMinTopMargin) {
180            y = mMinTopMargin;
181        }
182
183        // Don't allow the clock base to be under half of the screen
184        final float maxClockY = getMaxClockY();
185        if (y > maxClockY) {
186            y = maxClockY;
187        }
188
189        return (int) y;
190    }
191
192    private int getClockY() {
193        // Dark: Align the bottom edge of the clock at about half of the screen:
194        float clockYDark = getMaxClockY() + burnInPreventionOffsetY();
195        if (mPulsing) {
196            clockYDark -= mPulsingPadding;
197        }
198
199        float clockYRegular = getExpandedClockPosition();
200        boolean hasEnoughSpace = mMinTopMargin + mKeyguardStatusHeight < mBouncerTop;
201        float clockYTarget = mCurrentlySecure && hasEnoughSpace ?
202                mMinTopMargin : -mKeyguardStatusHeight;
203
204        // Move clock up while collapsing the shade
205        float shadeExpansion = Interpolators.FAST_OUT_LINEAR_IN.getInterpolation(mPanelExpansion);
206        final float clockY = MathUtils.lerp(clockYTarget, clockYRegular, shadeExpansion);
207
208        return (int) MathUtils.lerp(clockY, clockYDark, mDarkAmount);
209    }
210
211    /**
212     * We might want to fade out the clock when the user is swiping up.
213     * One exception is when the bouncer will become visible, in this cause the clock
214     * should always persist.
215     *
216     * @param y Current clock Y.
217     * @return Alpha from 0 to 1.
218     */
219    private float getClockAlpha(int y) {
220        float alphaKeyguard;
221        if (mCurrentlySecure) {
222            alphaKeyguard = 1;
223        } else {
224            alphaKeyguard = Math.max(0, y / Math.max(1f, getExpandedClockPosition()));
225            alphaKeyguard = Interpolators.ACCELERATE.getInterpolation(alphaKeyguard);
226        }
227        return MathUtils.lerp(alphaKeyguard, 1f, mDarkAmount);
228    }
229
230    private float burnInPreventionOffsetY() {
231        return zigzag(System.currentTimeMillis() / MILLIS_PER_MINUTES,
232                mBurnInPreventionOffsetY * 2,
233                BURN_IN_PREVENTION_PERIOD_Y)
234                - mBurnInPreventionOffsetY;
235    }
236
237    private float burnInPreventionOffsetX() {
238        return zigzag(System.currentTimeMillis() / MILLIS_PER_MINUTES,
239                mBurnInPreventionOffsetX * 2,
240                BURN_IN_PREVENTION_PERIOD_X)
241                - mBurnInPreventionOffsetX;
242    }
243
244    /**
245     * Implements a continuous, piecewise linear, periodic zig-zag function
246     *
247     * Can be thought of as a linear approximation of abs(sin(x)))
248     *
249     * @param period period of the function, ie. zigzag(x + period) == zigzag(x)
250     * @param amplitude maximum value of the function
251     * @return a value between 0 and amplitude
252     */
253    private float zigzag(float x, float amplitude, float period) {
254        float xprime = (x % period) / (period / 2);
255        float interpolationAmount = (xprime <= 1) ? xprime : (2 - xprime);
256        return interpolate(0, amplitude, interpolationAmount);
257    }
258
259    public static class Result {
260
261        /**
262         * The x translation of the clock.
263         */
264        public int clockX;
265
266        /**
267         * The y translation of the clock.
268         */
269        public int clockY;
270
271        /**
272         * The alpha value of the clock.
273         */
274        public float clockAlpha;
275
276        /**
277         * The top padding of the stack scroller, in pixels.
278         */
279        public int stackScrollerPadding;
280    }
281}
282