1 package com.android.keyguard;
2 
3 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
4 
5 import android.animation.Animator;
6 import android.animation.AnimatorListenerAdapter;
7 import android.animation.ValueAnimator;
8 import android.app.WallpaperManager;
9 import android.content.Context;
10 import android.graphics.Paint;
11 import android.graphics.Paint.Style;
12 import android.os.Build;
13 import android.transition.Transition;
14 import android.transition.TransitionListenerAdapter;
15 import android.transition.TransitionManager;
16 import android.transition.TransitionSet;
17 import android.transition.TransitionValues;
18 import android.util.AttributeSet;
19 import android.util.Log;
20 import android.util.MathUtils;
21 import android.view.View;
22 import android.view.ViewGroup;
23 import android.widget.FrameLayout;
24 import android.widget.RelativeLayout;
25 import android.widget.TextClock;
26 
27 import androidx.annotation.VisibleForTesting;
28 
29 import com.android.internal.colorextraction.ColorExtractor;
30 import com.android.internal.colorextraction.ColorExtractor.OnColorsChangedListener;
31 import com.android.keyguard.clock.ClockManager;
32 import com.android.systemui.Interpolators;
33 import com.android.systemui.colorextraction.SysuiColorExtractor;
34 import com.android.systemui.plugins.ClockPlugin;
35 import com.android.systemui.plugins.statusbar.StatusBarStateController;
36 import com.android.systemui.statusbar.StatusBarState;
37 
38 import java.io.FileDescriptor;
39 import java.io.PrintWriter;
40 import java.util.Arrays;
41 import java.util.TimeZone;
42 
43 import javax.inject.Inject;
44 import javax.inject.Named;
45 
46 /**
47  * Switch to show plugin clock when plugin is connected, otherwise it will show default clock.
48  */
49 public class KeyguardClockSwitch extends RelativeLayout {
50 
51     private static final String TAG = "KeyguardClockSwitch";
52     private static final boolean CUSTOM_CLOCKS_ENABLED = false;
53 
54     /**
55      * Animation fraction when text is transitioned to/from bold.
56      */
57     private static final float TO_BOLD_TRANSITION_FRACTION = 0.7f;
58 
59     /**
60      * Controller used to track StatusBar state to know when to show the big_clock_container.
61      */
62     private final StatusBarStateController mStatusBarStateController;
63 
64     /**
65      * Color extractor used to apply colors from wallpaper to custom clock faces.
66      */
67     private final SysuiColorExtractor mSysuiColorExtractor;
68 
69     /**
70      * Manager used to know when to show a custom clock face.
71      */
72     private final ClockManager mClockManager;
73 
74     /**
75      * Layout transition that scales the default clock face.
76      */
77     private final Transition mTransition;
78 
79     private final ClockVisibilityTransition mClockTransition;
80     private final ClockVisibilityTransition mBoldClockTransition;
81 
82     /**
83      * Optional/alternative clock injected via plugin.
84      */
85     private ClockPlugin mClockPlugin;
86 
87     /**
88      * Default clock.
89      */
90     private TextClock mClockView;
91 
92     /**
93      * Default clock, bold version.
94      * Used to transition to bold when shrinking the default clock.
95      */
96     private TextClock mClockViewBold;
97 
98     /**
99      * Frame for default and custom clock.
100      */
101     private FrameLayout mSmallClockFrame;
102 
103     /**
104      * Container for big custom clock.
105      */
106     private ViewGroup mBigClockContainer;
107 
108     /**
109      * Status area (date and other stuff) shown below the clock. Plugin can decide whether or not to
110      * show it below the alternate clock.
111      */
112     private View mKeyguardStatusArea;
113 
114     /**
115      * Maintain state so that a newly connected plugin can be initialized.
116      */
117     private float mDarkAmount;
118 
119     /**
120      * If the Keyguard Slice has a header (big center-aligned text.)
121      */
122     private boolean mShowingHeader;
123     private boolean mSupportsDarkText;
124     private int[] mColorPalette;
125 
126     /**
127      * Track the state of the status bar to know when to hide the big_clock_container.
128      */
129     private int mStatusBarState;
130 
131     private final StatusBarStateController.StateListener mStateListener =
132             new StatusBarStateController.StateListener() {
133                 @Override
134                 public void onStateChanged(int newState) {
135                     mStatusBarState = newState;
136                     updateBigClockVisibility();
137                 }
138             };
139 
140     private ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin;
141 
142     /**
143      * Listener for changes to the color palette.
144      *
145      * The color palette changes when the wallpaper is changed.
146      */
147     private final OnColorsChangedListener mColorsListener = (extractor, which) -> {
148         if ((which & WallpaperManager.FLAG_LOCK) != 0) {
149             updateColors();
150         }
151     };
152 
153     @Inject
KeyguardClockSwitch(@amedVIEW_CONTEXT) Context context, AttributeSet attrs, StatusBarStateController statusBarStateController, SysuiColorExtractor colorExtractor, ClockManager clockManager)154     public KeyguardClockSwitch(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
155             StatusBarStateController statusBarStateController, SysuiColorExtractor colorExtractor,
156             ClockManager clockManager) {
157         super(context, attrs);
158         mStatusBarStateController = statusBarStateController;
159         mStatusBarState = mStatusBarStateController.getState();
160         mSysuiColorExtractor = colorExtractor;
161         mClockManager = clockManager;
162 
163         mClockTransition = new ClockVisibilityTransition().setCutoff(
164                 1 - TO_BOLD_TRANSITION_FRACTION);
165         mClockTransition.addTarget(R.id.default_clock_view);
166         mBoldClockTransition = new ClockVisibilityTransition().setCutoff(
167                 TO_BOLD_TRANSITION_FRACTION);
168         mBoldClockTransition.addTarget(R.id.default_clock_view_bold);
169         mTransition = new TransitionSet()
170                 .setOrdering(TransitionSet.ORDERING_TOGETHER)
171                 .addTransition(mClockTransition)
172                 .addTransition(mBoldClockTransition)
173                 .setDuration(KeyguardSliceView.DEFAULT_ANIM_DURATION / 2)
174                 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
175     }
176 
177     /**
178      * Returns if this view is presenting a custom clock, or the default implementation.
179      */
hasCustomClock()180     public boolean hasCustomClock() {
181         return mClockPlugin != null;
182     }
183 
184     @Override
onFinishInflate()185     protected void onFinishInflate() {
186         super.onFinishInflate();
187         mClockView = findViewById(R.id.default_clock_view);
188         mClockViewBold = findViewById(R.id.default_clock_view_bold);
189         mSmallClockFrame = findViewById(R.id.clock_view);
190         mKeyguardStatusArea = findViewById(R.id.keyguard_status_area);
191     }
192 
193     @Override
onAttachedToWindow()194     protected void onAttachedToWindow() {
195         super.onAttachedToWindow();
196         if (CUSTOM_CLOCKS_ENABLED) {
197             mClockManager.addOnClockChangedListener(mClockChangedListener);
198         }
199         mStatusBarStateController.addCallback(mStateListener);
200         mSysuiColorExtractor.addOnColorsChangedListener(mColorsListener);
201         updateColors();
202     }
203 
204     @Override
onDetachedFromWindow()205     protected void onDetachedFromWindow() {
206         super.onDetachedFromWindow();
207         if (CUSTOM_CLOCKS_ENABLED) {
208             mClockManager.removeOnClockChangedListener(mClockChangedListener);
209         }
210         mStatusBarStateController.removeCallback(mStateListener);
211         mSysuiColorExtractor.removeOnColorsChangedListener(mColorsListener);
212         setClockPlugin(null);
213     }
214 
setClockPlugin(ClockPlugin plugin)215     private void setClockPlugin(ClockPlugin plugin) {
216         // Disconnect from existing plugin.
217         if (mClockPlugin != null) {
218             View smallClockView = mClockPlugin.getView();
219             if (smallClockView != null && smallClockView.getParent() == mSmallClockFrame) {
220                 mSmallClockFrame.removeView(smallClockView);
221             }
222             if (mBigClockContainer != null) {
223                 mBigClockContainer.removeAllViews();
224                 updateBigClockVisibility();
225             }
226             mClockPlugin.onDestroyView();
227             mClockPlugin = null;
228         }
229         if (plugin == null) {
230             mClockView.setVisibility(View.VISIBLE);
231             mClockViewBold.setVisibility(View.INVISIBLE);
232             mKeyguardStatusArea.setVisibility(View.VISIBLE);
233             return;
234         }
235         // Attach small and big clock views to hierarchy.
236         View smallClockView = plugin.getView();
237         if (smallClockView != null) {
238             mSmallClockFrame.addView(smallClockView, -1,
239                     new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
240                             ViewGroup.LayoutParams.WRAP_CONTENT));
241             mClockView.setVisibility(View.GONE);
242             mClockViewBold.setVisibility(View.GONE);
243         }
244         View bigClockView = plugin.getBigClockView();
245         if (bigClockView != null && mBigClockContainer != null) {
246             mBigClockContainer.addView(bigClockView);
247             updateBigClockVisibility();
248         }
249         // Hide default clock.
250         if (!plugin.shouldShowStatusArea()) {
251             mKeyguardStatusArea.setVisibility(View.GONE);
252         }
253         // Initialize plugin parameters.
254         mClockPlugin = plugin;
255         mClockPlugin.setStyle(getPaint().getStyle());
256         mClockPlugin.setTextColor(getCurrentTextColor());
257         mClockPlugin.setDarkAmount(mDarkAmount);
258         if (mColorPalette != null) {
259             mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette);
260         }
261     }
262 
263     /**
264      * Set container for big clock face appearing behind NSSL and KeyguardStatusView.
265      */
setBigClockContainer(ViewGroup container)266     public void setBigClockContainer(ViewGroup container) {
267         if (mClockPlugin != null && container != null) {
268             View bigClockView = mClockPlugin.getBigClockView();
269             if (bigClockView != null) {
270                 container.addView(bigClockView);
271             }
272         }
273         mBigClockContainer = container;
274         updateBigClockVisibility();
275     }
276 
277     /**
278      * It will also update plugin setStyle if plugin is connected.
279      */
setStyle(Style style)280     public void setStyle(Style style) {
281         mClockView.getPaint().setStyle(style);
282         mClockViewBold.getPaint().setStyle(style);
283         if (mClockPlugin != null) {
284             mClockPlugin.setStyle(style);
285         }
286     }
287 
288     /**
289      * It will also update plugin setTextColor if plugin is connected.
290      */
setTextColor(int color)291     public void setTextColor(int color) {
292         mClockView.setTextColor(color);
293         mClockViewBold.setTextColor(color);
294         if (mClockPlugin != null) {
295             mClockPlugin.setTextColor(color);
296         }
297     }
298 
setShowCurrentUserTime(boolean showCurrentUserTime)299     public void setShowCurrentUserTime(boolean showCurrentUserTime) {
300         mClockView.setShowCurrentUserTime(showCurrentUserTime);
301         mClockViewBold.setShowCurrentUserTime(showCurrentUserTime);
302     }
303 
setTextSize(int unit, float size)304     public void setTextSize(int unit, float size) {
305         mClockView.setTextSize(unit, size);
306     }
307 
setFormat12Hour(CharSequence format)308     public void setFormat12Hour(CharSequence format) {
309         mClockView.setFormat12Hour(format);
310         mClockViewBold.setFormat12Hour(format);
311     }
312 
setFormat24Hour(CharSequence format)313     public void setFormat24Hour(CharSequence format) {
314         mClockView.setFormat24Hour(format);
315         mClockViewBold.setFormat24Hour(format);
316     }
317 
318     /**
319      * Set the amount (ratio) that the device has transitioned to doze.
320      *
321      * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake.
322      */
setDarkAmount(float darkAmount)323     public void setDarkAmount(float darkAmount) {
324         mDarkAmount = darkAmount;
325         if (mClockPlugin != null) {
326             mClockPlugin.setDarkAmount(darkAmount);
327         }
328     }
329 
getPaint()330     public Paint getPaint() {
331         return mClockView.getPaint();
332     }
333 
getCurrentTextColor()334     public int getCurrentTextColor() {
335         return mClockView.getCurrentTextColor();
336     }
337 
getTextSize()338     public float getTextSize() {
339         return mClockView.getTextSize();
340     }
341 
342     /**
343      * Returns the preferred Y position of the clock.
344      *
345      * @param totalHeight Height of the parent container.
346      * @return preferred Y position.
347      */
getPreferredY(int totalHeight)348     int getPreferredY(int totalHeight) {
349         if (mClockPlugin != null) {
350             return mClockPlugin.getPreferredY(totalHeight);
351         } else {
352             return totalHeight / 2;
353         }
354     }
355 
356     /**
357      * Refresh the time of the clock, due to either time tick broadcast or doze time tick alarm.
358      */
refresh()359     public void refresh() {
360         mClockView.refresh();
361         mClockViewBold.refresh();
362         if (mClockPlugin != null) {
363             mClockPlugin.onTimeTick();
364         }
365         if (Build.IS_DEBUGGABLE) {
366             // Log for debugging b/130888082 (sysui waking up, but clock not updating)
367             Log.d(TAG, "Updating clock: " + mClockView.getText());
368         }
369     }
370 
371     /**
372      * Notifies that the time zone has changed.
373      */
onTimeZoneChanged(TimeZone timeZone)374     public void onTimeZoneChanged(TimeZone timeZone) {
375         if (mClockPlugin != null) {
376             mClockPlugin.onTimeZoneChanged(timeZone);
377         }
378     }
379 
updateColors()380     private void updateColors() {
381         ColorExtractor.GradientColors colors = mSysuiColorExtractor.getColors(
382                 WallpaperManager.FLAG_LOCK);
383         mSupportsDarkText = colors.supportsDarkText();
384         mColorPalette = colors.getColorPalette();
385         if (mClockPlugin != null) {
386             mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette);
387         }
388     }
389 
updateBigClockVisibility()390     private void updateBigClockVisibility() {
391         if (mBigClockContainer == null) {
392             return;
393         }
394         final boolean inDisplayState = mStatusBarState == StatusBarState.KEYGUARD
395                 || mStatusBarState == StatusBarState.SHADE_LOCKED;
396         final int visibility =
397                 inDisplayState && mBigClockContainer.getChildCount() != 0 ? View.VISIBLE
398                         : View.GONE;
399         if (mBigClockContainer.getVisibility() != visibility) {
400             mBigClockContainer.setVisibility(visibility);
401         }
402     }
403 
404     /**
405      * Sets if the keyguard slice is showing a center-aligned header. We need a smaller clock in
406      * these cases.
407      */
setKeyguardShowingHeader(boolean hasHeader)408     void setKeyguardShowingHeader(boolean hasHeader) {
409         if (mShowingHeader == hasHeader || hasCustomClock()) {
410             return;
411         }
412         mShowingHeader = hasHeader;
413 
414         float smallFontSize = mContext.getResources().getDimensionPixelSize(
415                 R.dimen.widget_small_font_size);
416         float bigFontSize = mContext.getResources().getDimensionPixelSize(
417                 R.dimen.widget_big_font_size);
418         mClockTransition.setScale(smallFontSize / bigFontSize);
419         mBoldClockTransition.setScale(bigFontSize / smallFontSize);
420 
421         // End any current transitions before starting a new transition so that the new transition
422         // starts from a good state instead of a potentially bad intermediate state arrived at
423         // during a transition animation.
424         TransitionManager.endTransitions((ViewGroup) mClockView.getParent());
425 
426         if (hasHeader) {
427             // After the transition, make the default clock GONE so that it doesn't make the
428             // KeyguardStatusView appear taller in KeyguardClockPositionAlgorithm and elsewhere.
429             mTransition.addListener(new TransitionListenerAdapter() {
430                 @Override
431                 public void onTransitionEnd(Transition transition) {
432                     super.onTransitionEnd(transition);
433                     // Check that header is actually showing. I saw issues where this event was
434                     // fired after the big clock transitioned back to visible, which causes the time
435                     // to completely disappear.
436                     if (mShowingHeader) {
437                         mClockView.setVisibility(View.GONE);
438                     }
439                     transition.removeListener(this);
440                 }
441             });
442         }
443 
444         TransitionManager.beginDelayedTransition((ViewGroup) mClockView.getParent(), mTransition);
445         mClockView.setVisibility(hasHeader ? View.INVISIBLE : View.VISIBLE);
446         mClockViewBold.setVisibility(hasHeader ? View.VISIBLE : View.INVISIBLE);
447         int paddingBottom = mContext.getResources().getDimensionPixelSize(hasHeader
448                 ? R.dimen.widget_vertical_padding_clock : R.dimen.title_clock_padding);
449         mClockView.setPadding(mClockView.getPaddingLeft(), mClockView.getPaddingTop(),
450                 mClockView.getPaddingRight(), paddingBottom);
451         mClockViewBold.setPadding(mClockViewBold.getPaddingLeft(), mClockViewBold.getPaddingTop(),
452                 mClockViewBold.getPaddingRight(), paddingBottom);
453     }
454 
455     @VisibleForTesting(otherwise = VisibleForTesting.NONE)
getClockChangedListener()456     ClockManager.ClockChangedListener getClockChangedListener() {
457         return mClockChangedListener;
458     }
459 
460     @VisibleForTesting(otherwise = VisibleForTesting.NONE)
getStateListener()461     StatusBarStateController.StateListener getStateListener() {
462         return mStateListener;
463     }
464 
dump(FileDescriptor fd, PrintWriter pw, String[] args)465     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
466         pw.println("KeyguardClockSwitch:");
467         pw.println("  mClockPlugin: " + mClockPlugin);
468         pw.println("  mClockView: " + mClockView);
469         pw.println("  mClockViewBold: " + mClockViewBold);
470         pw.println("  mSmallClockFrame: " + mSmallClockFrame);
471         pw.println("  mBigClockContainer: " + mBigClockContainer);
472         pw.println("  mKeyguardStatusArea: " + mKeyguardStatusArea);
473         pw.println("  mDarkAmount: " + mDarkAmount);
474         pw.println("  mShowingHeader: " + mShowingHeader);
475         pw.println("  mSupportsDarkText: " + mSupportsDarkText);
476         pw.println("  mColorPalette: " + Arrays.toString(mColorPalette));
477     }
478 
479     /**
480      * {@link Visibility} transformation that scales the view while it is disappearing/appearing and
481      * transitions suddenly at a cutoff fraction during the animation.
482      */
483     private class ClockVisibilityTransition extends android.transition.Visibility {
484 
485         private static final String PROPNAME_VISIBILITY = "systemui:keyguard:visibility";
486 
487         private float mCutoff;
488         private float mScale;
489 
490         /**
491          * Constructs a transition that switches between visible/invisible at a cutoff and scales in
492          * size while appearing/disappearing.
493          */
ClockVisibilityTransition()494         ClockVisibilityTransition() {
495             setCutoff(1f);
496             setScale(1f);
497         }
498 
499         /**
500          * Sets the transition point between visible/invisible.
501          *
502          * @param cutoff The fraction in [0, 1] when the view switches between visible/invisible.
503          * @return This transition object
504          */
setCutoff(float cutoff)505         public ClockVisibilityTransition setCutoff(float cutoff) {
506             mCutoff = cutoff;
507             return this;
508         }
509 
510         /**
511          * Sets the scale factor applied while appearing/disappearing.
512          *
513          * @param scale Scale factor applied while appearing/disappearing. When factor is less than
514          *              one, the view will shrink while disappearing. When it is greater than one,
515          *              the view will expand while disappearing.
516          * @return This transition object
517          */
setScale(float scale)518         public ClockVisibilityTransition setScale(float scale) {
519             mScale = scale;
520             return this;
521         }
522 
523         @Override
captureStartValues(TransitionValues transitionValues)524         public void captureStartValues(TransitionValues transitionValues) {
525             super.captureStartValues(transitionValues);
526             captureVisibility(transitionValues);
527         }
528 
529         @Override
captureEndValues(TransitionValues transitionValues)530         public void captureEndValues(TransitionValues transitionValues) {
531             super.captureStartValues(transitionValues);
532             captureVisibility(transitionValues);
533         }
534 
captureVisibility(TransitionValues transitionValues)535         private void captureVisibility(TransitionValues transitionValues) {
536             transitionValues.values.put(PROPNAME_VISIBILITY,
537                     transitionValues.view.getVisibility());
538         }
539 
540         @Override
onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)541         public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
542                 TransitionValues endValues) {
543             if (!sceneRoot.isShown()) {
544                 return null;
545             }
546             final float cutoff = mCutoff;
547             final int startVisibility = View.INVISIBLE;
548             final int endVisibility = (int) endValues.values.get(PROPNAME_VISIBILITY);
549             final float startScale = mScale;
550             final float endScale = 1f;
551             return createAnimator(view, cutoff, startVisibility, endVisibility, startScale,
552                     endScale);
553         }
554 
555         @Override
onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)556         public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
557                 TransitionValues endValues) {
558             if (!sceneRoot.isShown()) {
559                 return null;
560             }
561             final float cutoff = 1f - mCutoff;
562             final int startVisibility = View.VISIBLE;
563             final int endVisibility = (int) endValues.values.get(PROPNAME_VISIBILITY);
564             final float startScale = 1f;
565             final float endScale = mScale;
566             return createAnimator(view, cutoff, startVisibility, endVisibility, startScale,
567                     endScale);
568         }
569 
createAnimator(View view, float cutoff, int startVisibility, int endVisibility, float startScale, float endScale)570         private Animator createAnimator(View view, float cutoff, int startVisibility,
571                 int endVisibility, float startScale, float endScale) {
572             view.setPivotY(view.getHeight() - view.getPaddingBottom());
573             ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
574             animator.addUpdateListener(animation -> {
575                 final float fraction = animation.getAnimatedFraction();
576                 if (fraction > cutoff) {
577                     view.setVisibility(endVisibility);
578                 }
579                 final float scale = MathUtils.lerp(startScale, endScale, fraction);
580                 view.setScaleX(scale);
581                 view.setScaleY(scale);
582             });
583             animator.addListener(new AnimatorListenerAdapter() {
584                 @Override
585                 public void onAnimationStart(Animator animation) {
586                     super.onAnimationStart(animation);
587                     view.setVisibility(startVisibility);
588                     animation.removeListener(this);
589                 }
590             });
591             addListener(new TransitionListenerAdapter() {
592                 @Override
593                 public void onTransitionEnd(Transition transition) {
594                     view.setVisibility(endVisibility);
595                     view.setScaleX(1f);
596                     view.setScaleY(1f);
597                     transition.removeListener(this);
598                 }
599             });
600             return animator;
601         }
602     }
603 }
604