1 package com.android.keyguard;
2 
3 import android.animation.Animator;
4 import android.animation.AnimatorListenerAdapter;
5 import android.animation.AnimatorSet;
6 import android.animation.ObjectAnimator;
7 import android.content.Context;
8 import android.graphics.Paint;
9 import android.graphics.Paint.Style;
10 import android.util.AttributeSet;
11 import android.util.TypedValue;
12 import android.view.View;
13 import android.view.ViewGroup;
14 import android.widget.FrameLayout;
15 import android.widget.RelativeLayout;
16 
17 import androidx.annotation.IntDef;
18 import androidx.annotation.VisibleForTesting;
19 
20 import com.android.internal.colorextraction.ColorExtractor;
21 import com.android.keyguard.dagger.KeyguardStatusViewScope;
22 import com.android.systemui.R;
23 import com.android.systemui.animation.Interpolators;
24 import com.android.systemui.plugins.ClockPlugin;
25 
26 import java.io.FileDescriptor;
27 import java.io.PrintWriter;
28 import java.lang.annotation.Retention;
29 import java.lang.annotation.RetentionPolicy;
30 import java.util.Arrays;
31 import java.util.TimeZone;
32 
33 /**
34  * Switch to show plugin clock when plugin is connected, otherwise it will show default clock.
35  */
36 @KeyguardStatusViewScope
37 public class KeyguardClockSwitch extends RelativeLayout {
38 
39     private static final String TAG = "KeyguardClockSwitch";
40 
41     private static final long CLOCK_OUT_MILLIS = 150;
42     private static final long CLOCK_IN_MILLIS = 200;
43     private static final long STATUS_AREA_MOVE_MILLIS = 350;
44 
45     @IntDef({LARGE, SMALL})
46     @Retention(RetentionPolicy.SOURCE)
47     public @interface ClockSize { }
48 
49     public static final int LARGE = 0;
50     public static final int SMALL = 1;
51 
52     /**
53      * Optional/alternative clock injected via plugin.
54      */
55     private ClockPlugin mClockPlugin;
56 
57     /**
58      * Frame for small/large clocks
59      */
60     private FrameLayout mClockFrame;
61     private FrameLayout mLargeClockFrame;
62     private AnimatableClockView mClockView;
63     private AnimatableClockView mLargeClockView;
64 
65     private View mStatusArea;
66     private int mSmartspaceTopOffset;
67 
68     /**
69      * Maintain state so that a newly connected plugin can be initialized.
70      */
71     private float mDarkAmount;
72 
73     /**
74      * Indicates which clock is currently displayed - should be one of {@link ClockSize}.
75      * Use null to signify it is uninitialized.
76      */
77     @ClockSize private Integer mDisplayedClockSize = null;
78 
79     @VisibleForTesting AnimatorSet mClockInAnim = null;
80     @VisibleForTesting AnimatorSet mClockOutAnim = null;
81     private ObjectAnimator mStatusAreaAnim = null;
82 
83     /**
84      * If the Keyguard Slice has a header (big center-aligned text.)
85      */
86     private boolean mSupportsDarkText;
87     private int[] mColorPalette;
88 
89     private int mClockSwitchYAmount;
90     @VisibleForTesting boolean mChildrenAreLaidOut = false;
91 
KeyguardClockSwitch(Context context, AttributeSet attrs)92     public KeyguardClockSwitch(Context context, AttributeSet attrs) {
93         super(context, attrs);
94     }
95 
96     /**
97      * Apply dp changes on font/scale change
98      */
onDensityOrFontScaleChanged()99     public void onDensityOrFontScaleChanged() {
100         mLargeClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContext.getResources()
101                 .getDimensionPixelSize(R.dimen.large_clock_text_size));
102         mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContext.getResources()
103                 .getDimensionPixelSize(R.dimen.clock_text_size));
104 
105         mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize(
106                 R.dimen.keyguard_clock_switch_y_shift);
107 
108         mSmartspaceTopOffset = mContext.getResources().getDimensionPixelSize(
109                 R.dimen.keyguard_smartspace_top_offset);
110     }
111 
112     /**
113      * Returns if this view is presenting a custom clock, or the default implementation.
114      */
hasCustomClock()115     public boolean hasCustomClock() {
116         return mClockPlugin != null;
117     }
118 
119     @Override
onFinishInflate()120     protected void onFinishInflate() {
121         super.onFinishInflate();
122 
123         mClockFrame = findViewById(R.id.lockscreen_clock_view);
124         mClockView = findViewById(R.id.animatable_clock_view);
125         mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
126         mLargeClockView = findViewById(R.id.animatable_clock_view_large);
127         mStatusArea = findViewById(R.id.keyguard_status_area);
128 
129         onDensityOrFontScaleChanged();
130     }
131 
setClockPlugin(ClockPlugin plugin, int statusBarState)132     void setClockPlugin(ClockPlugin plugin, int statusBarState) {
133         // Disconnect from existing plugin.
134         if (mClockPlugin != null) {
135             View smallClockView = mClockPlugin.getView();
136             if (smallClockView != null && smallClockView.getParent() == mClockFrame) {
137                 mClockFrame.removeView(smallClockView);
138             }
139             View bigClockView = mClockPlugin.getBigClockView();
140             if (bigClockView != null && bigClockView.getParent() == mLargeClockFrame) {
141                 mLargeClockFrame.removeView(bigClockView);
142             }
143             mClockPlugin.onDestroyView();
144             mClockPlugin = null;
145         }
146         if (plugin == null) {
147             mClockView.setVisibility(View.VISIBLE);
148             mLargeClockView.setVisibility(View.VISIBLE);
149             return;
150         }
151         // Attach small and big clock views to hierarchy.
152         View smallClockView = plugin.getView();
153         if (smallClockView != null) {
154             mClockFrame.addView(smallClockView, -1,
155                     new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
156                             ViewGroup.LayoutParams.WRAP_CONTENT));
157             mClockView.setVisibility(View.GONE);
158         }
159         View bigClockView = plugin.getBigClockView();
160         if (bigClockView != null) {
161             mLargeClockFrame.addView(bigClockView);
162             mLargeClockView.setVisibility(View.GONE);
163         }
164 
165         // Initialize plugin parameters.
166         mClockPlugin = plugin;
167         mClockPlugin.setStyle(getPaint().getStyle());
168         mClockPlugin.setTextColor(getCurrentTextColor());
169         mClockPlugin.setDarkAmount(mDarkAmount);
170         if (mColorPalette != null) {
171             mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette);
172         }
173     }
174 
175     /**
176      * It will also update plugin setStyle if plugin is connected.
177      */
setStyle(Style style)178     public void setStyle(Style style) {
179         if (mClockPlugin != null) {
180             mClockPlugin.setStyle(style);
181         }
182     }
183 
184     /**
185      * It will also update plugin setTextColor if plugin is connected.
186      */
setTextColor(int color)187     public void setTextColor(int color) {
188         if (mClockPlugin != null) {
189             mClockPlugin.setTextColor(color);
190         }
191     }
192 
animateClockChange(boolean useLargeClock)193     private void animateClockChange(boolean useLargeClock) {
194         if (mClockInAnim != null) mClockInAnim.cancel();
195         if (mClockOutAnim != null) mClockOutAnim.cancel();
196         if (mStatusAreaAnim != null) mStatusAreaAnim.cancel();
197 
198         View in, out;
199         int direction = 1;
200         float statusAreaYTranslation;
201         if (useLargeClock) {
202             out = mClockFrame;
203             in = mLargeClockFrame;
204             if (indexOfChild(in) == -1) addView(in);
205             direction = -1;
206             statusAreaYTranslation = mClockFrame.getTop() - mStatusArea.getTop()
207                     + mSmartspaceTopOffset;
208         } else {
209             in = mClockFrame;
210             out = mLargeClockFrame;
211             statusAreaYTranslation = 0f;
212 
213             // Must remove in order for notifications to appear in the proper place
214             removeView(out);
215         }
216 
217         mClockOutAnim = new AnimatorSet();
218         mClockOutAnim.setDuration(CLOCK_OUT_MILLIS);
219         mClockOutAnim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
220         mClockOutAnim.playTogether(
221                 ObjectAnimator.ofFloat(out, View.ALPHA, 0f),
222                 ObjectAnimator.ofFloat(out, View.TRANSLATION_Y, 0,
223                         direction * -mClockSwitchYAmount));
224         mClockOutAnim.addListener(new AnimatorListenerAdapter() {
225             public void onAnimationEnd(Animator animation) {
226                 mClockOutAnim = null;
227             }
228         });
229 
230         in.setAlpha(0);
231         in.setVisibility(View.VISIBLE);
232         mClockInAnim = new AnimatorSet();
233         mClockInAnim.setDuration(CLOCK_IN_MILLIS);
234         mClockInAnim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
235         mClockInAnim.playTogether(ObjectAnimator.ofFloat(in, View.ALPHA, 1f),
236                 ObjectAnimator.ofFloat(in, View.TRANSLATION_Y, direction * mClockSwitchYAmount, 0));
237         mClockInAnim.setStartDelay(CLOCK_OUT_MILLIS / 2);
238         mClockInAnim.addListener(new AnimatorListenerAdapter() {
239             public void onAnimationEnd(Animator animation) {
240                 mClockInAnim = null;
241             }
242         });
243 
244         mClockInAnim.start();
245         mClockOutAnim.start();
246 
247         mStatusAreaAnim = ObjectAnimator.ofFloat(mStatusArea, View.TRANSLATION_Y,
248                 statusAreaYTranslation);
249         mStatusAreaAnim.setDuration(STATUS_AREA_MOVE_MILLIS);
250         mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
251         mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
252             public void onAnimationEnd(Animator animation) {
253                 mStatusAreaAnim = null;
254             }
255         });
256         mStatusAreaAnim.start();
257     }
258 
259     /**
260      * Set the amount (ratio) that the device has transitioned to doze.
261      *
262      * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake.
263      */
setDarkAmount(float darkAmount)264     public void setDarkAmount(float darkAmount) {
265         mDarkAmount = darkAmount;
266         if (mClockPlugin != null) {
267             mClockPlugin.setDarkAmount(darkAmount);
268         }
269     }
270 
271     /**
272      * Display the desired clock and hide the other one
273      *
274      * @return true if desired clock appeared and false if it was already visible
275      */
switchToClock(@lockSize int clockSize)276     boolean switchToClock(@ClockSize int clockSize) {
277         if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) {
278             return false;
279         }
280 
281         // let's make sure clock is changed only after all views were laid out so we can
282         // translate them properly
283         if (mChildrenAreLaidOut) {
284             animateClockChange(clockSize == LARGE);
285         }
286 
287         mDisplayedClockSize = clockSize;
288         return true;
289     }
290 
291     @Override
onLayout(boolean changed, int l, int t, int r, int b)292     protected void onLayout(boolean changed, int l, int t, int r, int b) {
293         super.onLayout(changed, l, t, r, b);
294 
295         if (mDisplayedClockSize != null && !mChildrenAreLaidOut) {
296             animateClockChange(mDisplayedClockSize == LARGE);
297         }
298 
299         mChildrenAreLaidOut = true;
300     }
301 
getPaint()302     public Paint getPaint() {
303         return mClockView.getPaint();
304     }
305 
getCurrentTextColor()306     public int getCurrentTextColor() {
307         return mClockView.getCurrentTextColor();
308     }
309 
getTextSize()310     public float getTextSize() {
311         return mClockView.getTextSize();
312     }
313 
314     /**
315      * Refresh the time of the clock, due to either time tick broadcast or doze time tick alarm.
316      */
refresh()317     public void refresh() {
318         if (mClockPlugin != null) {
319             mClockPlugin.onTimeTick();
320         }
321     }
322 
323     /**
324      * Notifies that the time zone has changed.
325      */
onTimeZoneChanged(TimeZone timeZone)326     public void onTimeZoneChanged(TimeZone timeZone) {
327         if (mClockPlugin != null) {
328             mClockPlugin.onTimeZoneChanged(timeZone);
329         }
330     }
331 
332     /**
333      * Notifies that the time format has changed.
334      *
335      * @param timeFormat "12" for 12-hour format, "24" for 24-hour format
336      */
onTimeFormatChanged(String timeFormat)337     public void onTimeFormatChanged(String timeFormat) {
338         if (mClockPlugin != null) {
339             mClockPlugin.onTimeFormatChanged(timeFormat);
340         }
341     }
342 
updateColors(ColorExtractor.GradientColors colors)343     void updateColors(ColorExtractor.GradientColors colors) {
344         mSupportsDarkText = colors.supportsDarkText();
345         mColorPalette = colors.getColorPalette();
346         if (mClockPlugin != null) {
347             mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette);
348         }
349     }
350 
dump(FileDescriptor fd, PrintWriter pw, String[] args)351     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
352         pw.println("KeyguardClockSwitch:");
353         pw.println("  mClockPlugin: " + mClockPlugin);
354         pw.println("  mClockFrame: " + mClockFrame);
355         pw.println("  mLargeClockFrame: " + mLargeClockFrame);
356         pw.println("  mStatusArea: " + mStatusArea);
357         pw.println("  mDarkAmount: " + mDarkAmount);
358         pw.println("  mSupportsDarkText: " + mSupportsDarkText);
359         pw.println("  mColorPalette: " + Arrays.toString(mColorPalette));
360         pw.println("  mDisplayedClockSize: " + mDisplayedClockSize);
361     }
362 }
363