1/*
2 * Copyright (C) 2016 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 android.animation.ValueAnimator;
20import android.content.Context;
21import android.os.Bundle;
22import android.os.Handler;
23import android.os.SystemClock;
24import android.util.TimeUtils;
25
26import com.android.systemui.Dumpable;
27import com.android.systemui.Dependency;
28import com.android.systemui.Interpolators;
29import com.android.systemui.SysUiServiceProvider;
30import com.android.systemui.statusbar.CommandQueue;
31import com.android.systemui.statusbar.CommandQueue.Callbacks;
32import com.android.systemui.statusbar.policy.KeyguardMonitor;
33
34import java.io.FileDescriptor;
35import java.io.PrintWriter;
36
37/**
38 * Class to control all aspects about light bar changes.
39 */
40public class LightBarTransitionsController implements Dumpable, Callbacks {
41
42    public static final long DEFAULT_TINT_ANIMATION_DURATION = 120;
43    private static final String EXTRA_DARK_INTENSITY = "dark_intensity";
44
45    private final Handler mHandler;
46    private final DarkIntensityApplier mApplier;
47    private final KeyguardMonitor mKeyguardMonitor;
48
49    private boolean mTransitionDeferring;
50    private long mTransitionDeferringStartTime;
51    private long mTransitionDeferringDuration;
52    private boolean mTransitionPending;
53    private boolean mTintChangePending;
54    private float mPendingDarkIntensity;
55    private ValueAnimator mTintAnimator;
56    private float mDarkIntensity;
57    private float mNextDarkIntensity;
58
59    private final Runnable mTransitionDeferringDoneRunnable = new Runnable() {
60        @Override
61        public void run() {
62            mTransitionDeferring = false;
63        }
64    };
65
66    public LightBarTransitionsController(Context context, DarkIntensityApplier applier) {
67        mApplier = applier;
68        mHandler = new Handler();
69        mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
70        SysUiServiceProvider.getComponent(context, CommandQueue.class)
71                .addCallbacks(this);
72    }
73
74    public void destroy(Context context) {
75        SysUiServiceProvider.getComponent(context, CommandQueue.class)
76                .removeCallbacks(this);
77    }
78
79    public void saveState(Bundle outState) {
80        float intensity = mTintAnimator != null && mTintAnimator.isRunning()
81                ?  mNextDarkIntensity : mDarkIntensity;
82        outState.putFloat(EXTRA_DARK_INTENSITY, intensity);
83    }
84
85    public void restoreState(Bundle savedInstanceState) {
86        setIconTintInternal(savedInstanceState.getFloat(EXTRA_DARK_INTENSITY, 0));
87    }
88
89    @Override
90    public void appTransitionPending(boolean forced) {
91        if (mKeyguardMonitor.isKeyguardGoingAway() && !forced) {
92            return;
93        }
94        mTransitionPending = true;
95    }
96
97    @Override
98    public void appTransitionCancelled() {
99        if (mTransitionPending && mTintChangePending) {
100            mTintChangePending = false;
101            animateIconTint(mPendingDarkIntensity, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
102        }
103        mTransitionPending = false;
104    }
105
106    @Override
107    public void appTransitionStarting(long startTime, long duration, boolean forced) {
108        if (mKeyguardMonitor.isKeyguardGoingAway() && !forced) {
109            return;
110        }
111        if (mTransitionPending && mTintChangePending) {
112            mTintChangePending = false;
113            animateIconTint(mPendingDarkIntensity,
114                    Math.max(0, startTime - SystemClock.uptimeMillis()),
115                    duration);
116
117        } else if (mTransitionPending) {
118
119            // If we don't have a pending tint change yet, the change might come in the future until
120            // startTime is reached.
121            mTransitionDeferring = true;
122            mTransitionDeferringStartTime = startTime;
123            mTransitionDeferringDuration = duration;
124            mHandler.removeCallbacks(mTransitionDeferringDoneRunnable);
125            mHandler.postAtTime(mTransitionDeferringDoneRunnable, startTime);
126        }
127        mTransitionPending = false;
128    }
129
130    public void setIconsDark(boolean dark, boolean animate) {
131        if (!animate) {
132            setIconTintInternal(dark ? 1.0f : 0.0f);
133        } else if (mTransitionPending) {
134            deferIconTintChange(dark ? 1.0f : 0.0f);
135        } else if (mTransitionDeferring) {
136            animateIconTint(dark ? 1.0f : 0.0f,
137                    Math.max(0, mTransitionDeferringStartTime - SystemClock.uptimeMillis()),
138                    mTransitionDeferringDuration);
139        } else {
140            animateIconTint(dark ? 1.0f : 0.0f, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
141        }
142    }
143
144    public float getCurrentDarkIntensity() {
145        return mDarkIntensity;
146    }
147
148    private void deferIconTintChange(float darkIntensity) {
149        if (mTintChangePending && darkIntensity == mPendingDarkIntensity) {
150            return;
151        }
152        mTintChangePending = true;
153        mPendingDarkIntensity = darkIntensity;
154    }
155
156    private void animateIconTint(float targetDarkIntensity, long delay,
157            long duration) {
158        if (mTintAnimator != null) {
159            mTintAnimator.cancel();
160        }
161        if (mDarkIntensity == targetDarkIntensity) {
162            return;
163        }
164        mNextDarkIntensity = targetDarkIntensity;
165        mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity);
166        mTintAnimator.addUpdateListener(
167                animation -> setIconTintInternal((Float) animation.getAnimatedValue()));
168        mTintAnimator.setDuration(duration);
169        mTintAnimator.setStartDelay(delay);
170        mTintAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
171        mTintAnimator.start();
172    }
173
174    private void setIconTintInternal(float darkIntensity) {
175        mDarkIntensity = darkIntensity;
176        mApplier.applyDarkIntensity(darkIntensity);
177    }
178
179    @Override
180    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
181        pw.print("  mTransitionDeferring="); pw.print(mTransitionDeferring);
182        if (mTransitionDeferring) {
183            pw.println();
184            pw.print("   mTransitionDeferringStartTime=");
185            pw.println(TimeUtils.formatUptime(mTransitionDeferringStartTime));
186
187            pw.print("   mTransitionDeferringDuration=");
188            TimeUtils.formatDuration(mTransitionDeferringDuration, pw);
189            pw.println();
190        }
191        pw.print("  mTransitionPending="); pw.print(mTransitionPending);
192        pw.print(" mTintChangePending="); pw.println(mTintChangePending);
193
194        pw.print("  mPendingDarkIntensity="); pw.print(mPendingDarkIntensity);
195        pw.print(" mDarkIntensity="); pw.print(mDarkIntensity);
196        pw.print(" mNextDarkIntensity="); pw.println(mNextDarkIntensity);
197    }
198
199    /**
200     * Interface to apply a specific dark intensity.
201     */
202    public interface DarkIntensityApplier {
203        void applyDarkIntensity(float darkIntensity);
204    }
205}
206