1 package com.android.keyguard;
2 
3 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN;
4 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN;
5 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE;
6 
7 import android.animation.Animator;
8 import android.animation.AnimatorListenerAdapter;
9 import android.animation.AnimatorSet;
10 import android.animation.ObjectAnimator;
11 import android.content.Context;
12 import android.graphics.Canvas;
13 import android.graphics.Rect;
14 import android.util.AttributeSet;
15 import android.view.View;
16 import android.view.ViewGroup;
17 import android.widget.RelativeLayout;
18 
19 import androidx.annotation.IntDef;
20 import androidx.annotation.VisibleForTesting;
21 import androidx.core.content.res.ResourcesCompat;
22 
23 import com.android.app.animation.Interpolators;
24 import com.android.keyguard.dagger.KeyguardStatusViewScope;
25 import com.android.systemui.keyguard.MigrateClocksToBlueprint;
26 import com.android.systemui.log.LogBuffer;
27 import com.android.systemui.log.core.LogLevel;
28 import com.android.systemui.plugins.clocks.ClockController;
29 import com.android.systemui.res.R;
30 import com.android.systemui.shared.clocks.DefaultClockController;
31 
32 import java.io.PrintWriter;
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 
36 /**
37  * Switch to show plugin clock when plugin is connected, otherwise it will show default clock.
38  */
39 @KeyguardStatusViewScope
40 public class KeyguardClockSwitch extends RelativeLayout {
41 
42     private static final String TAG = "KeyguardClockSwitch";
43     public static final String MISSING_CLOCK_ID = "CLOCK_MISSING";
44 
45     private static final long CLOCK_OUT_MILLIS = 133;
46     private static final long CLOCK_IN_MILLIS = 167;
47     public static final long CLOCK_IN_START_DELAY_MILLIS = 133;
48     private static final long STATUS_AREA_START_DELAY_MILLIS = 0;
49     private static final long STATUS_AREA_MOVE_UP_MILLIS = 967;
50     private static final long STATUS_AREA_MOVE_DOWN_MILLIS = 467;
51     private static final float SMARTSPACE_TRANSLATION_CENTER_MULTIPLIER = 1.4f;
52     private static final float SMARTSPACE_TOP_PADDING_MULTIPLIER = 2.625f;
53 
54     @IntDef({LARGE, SMALL})
55     @Retention(RetentionPolicy.SOURCE)
56     public @interface ClockSize { }
57 
58     public static final int LARGE = 0;
59     public static final int SMALL = 1;
60     // compensate for translation of parents subject to device screen
61     // In this case, the translation comes from KeyguardStatusView
62     public int screenOffsetYPadding = 0;
63 
64     /** Returns a region for the large clock to position itself, based on the given parent. */
getLargeClockRegion(ViewGroup parent)65     public static Rect getLargeClockRegion(ViewGroup parent) {
66         int largeClockTopMargin = parent.getResources()
67                 .getDimensionPixelSize(
68                         com.android.systemui.customization.R.dimen.keyguard_large_clock_top_margin);
69         int targetHeight = parent.getResources()
70                 .getDimensionPixelSize(
71                         com.android.systemui.customization.R.dimen.large_clock_text_size)
72                 * 2;
73         int top = parent.getHeight() / 2 - targetHeight / 2
74                 + largeClockTopMargin / 2;
75         return new Rect(
76                 parent.getLeft(),
77                 top,
78                 parent.getRight(),
79                 top + targetHeight);
80     }
81 
82     /** Returns a region for the small clock to position itself, based on the given parent. */
getSmallClockRegion(ViewGroup parent)83     public static Rect getSmallClockRegion(ViewGroup parent) {
84         int targetHeight = parent.getResources()
85                 .getDimensionPixelSize(
86                         com.android.systemui.customization.R.dimen.small_clock_text_size);
87         return new Rect(
88                 parent.getLeft(),
89                 parent.getTop(),
90                 parent.getRight(),
91                 parent.getTop() + targetHeight);
92     }
93 
94     /**
95      * Frame for small/large clocks
96      */
97     private KeyguardClockFrame mSmallClockFrame;
98     private KeyguardClockFrame mLargeClockFrame;
99     private ClockController mClock;
100 
101     // It's bc_smartspace_view, assigned by KeyguardClockSwitchController
102     // to get the top padding for translating smartspace for weather clock
103     private View mSmartspace;
104 
105     // Smartspace in weather clock is translated by this value
106     // to compensate for the position invisible dateWeatherView
107     private int mSmartspaceTop = -1;
108 
109     private KeyguardStatusAreaView mStatusArea;
110     private int mSmartspaceTopOffset;
111     private float mWeatherClockSmartspaceScaling = 1f;
112     private int mWeatherClockSmartspaceTranslateX = 0;
113     private int mWeatherClockSmartspaceTranslateY = 0;
114     private int mDrawAlpha = 255;
115 
116     private int mStatusBarHeight = 0;
117 
118     /**
119      * Maintain state so that a newly connected plugin can be initialized.
120      */
121     private float mDarkAmount;
122     private boolean mSplitShadeCentered = false;
123 
124     /**
125      * Indicates which clock is currently displayed - should be one of {@link ClockSize}.
126      * Use null to signify it is uninitialized.
127      */
128     @ClockSize private Integer mDisplayedClockSize = null;
129 
130     @VisibleForTesting AnimatorSet mClockInAnim = null;
131     @VisibleForTesting AnimatorSet mClockOutAnim = null;
132     @VisibleForTesting AnimatorSet mStatusAreaAnim = null;
133 
134     private int mClockSwitchYAmount;
135     @VisibleForTesting boolean mChildrenAreLaidOut = false;
136     @VisibleForTesting boolean mAnimateOnLayout = true;
137     private LogBuffer mLogBuffer = null;
138 
KeyguardClockSwitch(Context context, AttributeSet attrs)139     public KeyguardClockSwitch(Context context, AttributeSet attrs) {
140         super(context, attrs);
141     }
142 
143     /**
144      * Apply dp changes on configuration change
145      */
onConfigChanged()146     public void onConfigChanged() {
147         mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize(
148                 R.dimen.keyguard_clock_switch_y_shift);
149         mSmartspaceTopOffset = (int) (mContext.getResources().getDimensionPixelSize(
150                         R.dimen.keyguard_smartspace_top_offset)
151                 * mContext.getResources().getConfiguration().fontScale
152                 / mContext.getResources().getDisplayMetrics().density
153                 * SMARTSPACE_TOP_PADDING_MULTIPLIER);
154         mWeatherClockSmartspaceScaling = ResourcesCompat.getFloat(
155                 mContext.getResources(), R.dimen.weather_clock_smartspace_scale);
156         mWeatherClockSmartspaceTranslateX = mContext.getResources().getDimensionPixelSize(
157                 R.dimen.weather_clock_smartspace_translateX);
158         mWeatherClockSmartspaceTranslateY = mContext.getResources().getDimensionPixelSize(
159                 R.dimen.weather_clock_smartspace_translateY);
160         mStatusBarHeight = mContext.getResources().getDimensionPixelSize(
161                 R.dimen.status_bar_height);
162         updateStatusArea(/* animate= */false);
163     }
164 
165     /** Get bc_smartspace_view from KeyguardClockSwitchController
166      * Use its top to decide the translation value */
setSmartspace(View smartspace)167     public void setSmartspace(View smartspace) {
168         mSmartspace = smartspace;
169     }
170 
171     /** Sets whether the large clock is being shown on a connected display. */
setLargeClockOnSecondaryDisplay(boolean onSecondaryDisplay)172     public void setLargeClockOnSecondaryDisplay(boolean onSecondaryDisplay) {
173         if (mClock != null) {
174             mClock.getLargeClock().getEvents().onSecondaryDisplayChanged(onSecondaryDisplay);
175         }
176     }
177 
178     /**
179      * Enable or disable split shade specific positioning
180      */
setSplitShadeCentered(boolean splitShadeCentered)181     public void setSplitShadeCentered(boolean splitShadeCentered) {
182         if (mSplitShadeCentered != splitShadeCentered) {
183             mSplitShadeCentered = splitShadeCentered;
184             updateStatusArea(/* animate= */true);
185         }
186     }
187 
getSplitShadeCentered()188     public boolean getSplitShadeCentered() {
189         return mSplitShadeCentered;
190     }
191 
192     @Override
onFinishInflate()193     protected void onFinishInflate() {
194         super.onFinishInflate();
195         if (!MigrateClocksToBlueprint.isEnabled()) {
196             mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
197             mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
198             mStatusArea = findViewById(R.id.keyguard_status_area);
199         } else {
200             removeView(findViewById(R.id.lockscreen_clock_view));
201             removeView(findViewById(R.id.lockscreen_clock_view_large));
202         }
203         onConfigChanged();
204     }
205 
206     @Override
onSetAlpha(int alpha)207     protected boolean onSetAlpha(int alpha) {
208         mDrawAlpha = alpha;
209         return true;
210     }
211 
212     @Override
dispatchDraw(Canvas canvas)213     protected void dispatchDraw(Canvas canvas) {
214         KeyguardClockFrame.saveCanvasAlpha(
215                 this, canvas, mDrawAlpha,
216                 c -> {
217                     super.dispatchDraw(c);
218                     return kotlin.Unit.INSTANCE;
219                 });
220     }
221 
setLogBuffer(LogBuffer logBuffer)222     public void setLogBuffer(LogBuffer logBuffer) {
223         mLogBuffer = logBuffer;
224     }
225 
getLogBuffer()226     public LogBuffer getLogBuffer() {
227         return mLogBuffer;
228     }
229 
230     /** Returns the id of the currently rendering clock */
getClockId()231     public String getClockId() {
232         if (mClock == null) {
233             return MISSING_CLOCK_ID;
234         }
235         return mClock.getConfig().getId();
236     }
237 
setClock(ClockController clock, int statusBarState)238     void setClock(ClockController clock, int statusBarState) {
239         mClock = clock;
240 
241         // Disconnect from existing plugin.
242         mSmallClockFrame.removeAllViews();
243         mLargeClockFrame.removeAllViews();
244 
245         if (clock == null) {
246             if (mLogBuffer != null) {
247                 mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown");
248             }
249             return;
250         }
251 
252         // Attach small and big clock views to hierarchy.
253         if (mLogBuffer != null) {
254             mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch");
255         }
256         mSmallClockFrame.addView(clock.getSmallClock().getView());
257         mLargeClockFrame.addView(clock.getLargeClock().getView());
258         updateClockTargetRegions();
259         updateStatusArea(/* animate= */false);
260     }
261 
updateStatusArea(boolean animate)262     private void updateStatusArea(boolean animate) {
263         if (mDisplayedClockSize != null && mChildrenAreLaidOut) {
264             updateClockViews(mDisplayedClockSize == LARGE, animate);
265         }
266     }
267 
updateClockTargetRegions()268     void updateClockTargetRegions() {
269         if (MigrateClocksToBlueprint.isEnabled()) {
270             return;
271         }
272         if (mClock != null) {
273             if (mSmallClockFrame.isLaidOut()) {
274                 Rect targetRegion = getSmallClockRegion(mSmallClockFrame);
275                 mClock.getSmallClock().getEvents().onTargetRegionChanged(targetRegion);
276             }
277 
278             if (mLargeClockFrame.isLaidOut()) {
279                 Rect targetRegion = getLargeClockRegion(mLargeClockFrame);
280                 if (mClock instanceof DefaultClockController) {
281                     mClock.getLargeClock().getEvents().onTargetRegionChanged(
282                             targetRegion);
283                 } else {
284                     mClock.getLargeClock().getEvents().onTargetRegionChanged(
285                             new Rect(
286                                     targetRegion.left,
287                                     targetRegion.top - screenOffsetYPadding,
288                                     targetRegion.right,
289                                     targetRegion.bottom - screenOffsetYPadding));
290                 }
291             }
292         }
293     }
294 
updateClockViews(boolean useLargeClock, boolean animate)295     private void updateClockViews(boolean useLargeClock, boolean animate) {
296         if (mLogBuffer != null) {
297             mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> {
298                 msg.setBool1(useLargeClock);
299                 msg.setBool2(animate);
300                 msg.setBool3(mChildrenAreLaidOut);
301                 return kotlin.Unit.INSTANCE;
302             }, (msg) -> "updateClockViews"
303                     + "; useLargeClock=" + msg.getBool1()
304                     + "; animate=" + msg.getBool2()
305                     + "; mChildrenAreLaidOut=" + msg.getBool3());
306         }
307 
308         if (mClockInAnim != null) mClockInAnim.cancel();
309         if (mClockOutAnim != null) mClockOutAnim.cancel();
310         if (mStatusAreaAnim != null) mStatusAreaAnim.cancel();
311 
312         mClockInAnim = null;
313         mClockOutAnim = null;
314         mStatusAreaAnim = null;
315 
316         View in, out;
317         // statusAreaYTranslation uses for the translation for both mStatusArea and mSmallClockFrame
318         // statusAreaClockTranslateY only uses for mStatusArea
319         float statusAreaYTranslation, statusAreaClockScale = 1f;
320         float statusAreaClockTranslateX = 0f, statusAreaClockTranslateY = 0f;
321         float clockInYTranslation, clockOutYTranslation;
322         if (useLargeClock) {
323             out = mSmallClockFrame;
324             in = mLargeClockFrame;
325             if (indexOfChild(in) == -1) addView(in, 0);
326             statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop()
327                     + mSmartspaceTopOffset;
328             // TODO: Load from clock config when less risky
329             if (mClock != null
330                     && mClock.getLargeClock().getConfig().getHasCustomWeatherDataDisplay()) {
331                 statusAreaClockScale = mWeatherClockSmartspaceScaling;
332                 statusAreaClockTranslateX = mWeatherClockSmartspaceTranslateX;
333                 if (mSplitShadeCentered) {
334                     statusAreaClockTranslateX *= SMARTSPACE_TRANSLATION_CENTER_MULTIPLIER;
335                 }
336 
337                 // On large weather clock,
338                 // top padding for time is status bar height from top of the screen.
339                 // On small one,
340                 // it's screenOffsetYPadding (translationY for KeyguardStatusView),
341                 // Cause smartspace is positioned according to the smallClockFrame
342                 // we need to translate the difference between bottom of large clock and small clock
343                 // Also, we need to counter offset the empty date weather view, mSmartspaceTop
344                 // mWeatherClockSmartspaceTranslateY is only for Felix
345                 statusAreaClockTranslateY = mStatusBarHeight - 0.6F *  mSmallClockFrame.getHeight()
346                         - mSmartspaceTop - screenOffsetYPadding
347                         - statusAreaYTranslation + mWeatherClockSmartspaceTranslateY;
348             }
349             clockInYTranslation = 0;
350             clockOutYTranslation = 0; // Small clock translation is handled with statusArea
351         } else {
352             in = mSmallClockFrame;
353             out = mLargeClockFrame;
354             statusAreaYTranslation = 0f;
355             clockInYTranslation = 0f;
356             clockOutYTranslation = mClockSwitchYAmount * -1f;
357 
358             // Must remove in order for notifications to appear in the proper place, ideally this
359             // would happen after the out animation runs, but we can't guarantee that the
360             // nofications won't enter only after the out animation runs.
361             removeView(out);
362         }
363 
364         if (!animate) {
365             out.setAlpha(0f);
366             out.setTranslationY(clockOutYTranslation);
367             out.setVisibility(INVISIBLE);
368             in.setAlpha(1f);
369             in.setTranslationY(clockInYTranslation);
370             in.setVisibility(VISIBLE);
371             mStatusArea.setScaleX(statusAreaClockScale);
372             mStatusArea.setScaleY(statusAreaClockScale);
373             mStatusArea.setTranslateXFromClockDesign(statusAreaClockTranslateX);
374             mStatusArea.setTranslateYFromClockDesign(statusAreaClockTranslateY);
375             mStatusArea.setTranslateYFromClockSize(statusAreaYTranslation);
376             mSmallClockFrame.setTranslationY(statusAreaYTranslation);
377             return;
378         }
379 
380         mClockOutAnim = new AnimatorSet();
381         mClockOutAnim.setDuration(CLOCK_OUT_MILLIS);
382         mClockOutAnim.setInterpolator(Interpolators.LINEAR);
383         mClockOutAnim.playTogether(
384                 ObjectAnimator.ofFloat(out, ALPHA, 0f),
385                 ObjectAnimator.ofFloat(out, TRANSLATION_Y, clockOutYTranslation));
386         mClockOutAnim.addListener(new AnimatorListenerAdapter() {
387             public void onAnimationEnd(Animator animation) {
388                 if (mClockOutAnim == animation) {
389                     out.setVisibility(INVISIBLE);
390                     mClockOutAnim = null;
391                 }
392             }
393         });
394 
395         in.setVisibility(View.VISIBLE);
396         mClockInAnim = new AnimatorSet();
397         mClockInAnim.setDuration(CLOCK_IN_MILLIS);
398         mClockInAnim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
399         mClockInAnim.playTogether(
400                 ObjectAnimator.ofFloat(in, ALPHA, 1f),
401                 ObjectAnimator.ofFloat(in, TRANSLATION_Y, clockInYTranslation));
402         mClockInAnim.setStartDelay(CLOCK_IN_START_DELAY_MILLIS);
403         mClockInAnim.addListener(new AnimatorListenerAdapter() {
404             public void onAnimationEnd(Animator animation) {
405                 if (mClockInAnim == animation) {
406                     mClockInAnim = null;
407                 }
408             }
409         });
410 
411         mStatusAreaAnim = new AnimatorSet();
412         mStatusAreaAnim.setStartDelay(STATUS_AREA_START_DELAY_MILLIS);
413         mStatusAreaAnim.setDuration(
414                 useLargeClock ? STATUS_AREA_MOVE_UP_MILLIS : STATUS_AREA_MOVE_DOWN_MILLIS);
415         mStatusAreaAnim.setInterpolator(Interpolators.EMPHASIZED);
416         mStatusAreaAnim.playTogether(
417                 ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_Y_CLOCK_SIZE.getProperty(),
418                         statusAreaYTranslation),
419                 ObjectAnimator.ofFloat(mSmallClockFrame, TRANSLATION_Y, statusAreaYTranslation),
420                 ObjectAnimator.ofFloat(mStatusArea, SCALE_X, statusAreaClockScale),
421                 ObjectAnimator.ofFloat(mStatusArea, SCALE_Y, statusAreaClockScale),
422                 ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_X_CLOCK_DESIGN.getProperty(),
423                         statusAreaClockTranslateX),
424                 ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_Y_CLOCK_DESIGN.getProperty(),
425                         statusAreaClockTranslateY));
426         mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
427             public void onAnimationEnd(Animator animation) {
428                 if (mStatusAreaAnim == animation) {
429                     mStatusAreaAnim = null;
430                 }
431             }
432         });
433 
434         mClockInAnim.start();
435         mClockOutAnim.start();
436         mStatusAreaAnim.start();
437     }
438 
439     /**
440      * Display the desired clock and hide the other one
441      *
442      * @return true if desired clock appeared and false if it was already visible
443      */
switchToClock(@lockSize int clockSize, boolean animate)444     boolean switchToClock(@ClockSize int clockSize, boolean animate) {
445         if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) {
446             return false;
447         }
448 
449         // let's make sure clock is changed only after all views were laid out so we can
450         // translate them properly
451         if (mChildrenAreLaidOut) {
452             updateClockViews(clockSize == LARGE, animate);
453         }
454 
455         mDisplayedClockSize = clockSize;
456         return true;
457     }
458 
459     @Override
onLayout(boolean changed, int l, int t, int r, int b)460     protected void onLayout(boolean changed, int l, int t, int r, int b) {
461         super.onLayout(changed, l, t, r, b);
462         // TODO: b/305022530
463         if (mClock != null && mClock.getConfig().getId().equals("DIGITAL_CLOCK_METRO")) {
464             mClock.getEvents().onColorPaletteChanged(mContext.getResources());
465         }
466 
467         if (changed) {
468             post(() -> updateClockTargetRegions());
469         }
470 
471         if (mSmartspace != null && mSmartspaceTop != mSmartspace.getTop()
472                 && mDisplayedClockSize != null) {
473             mSmartspaceTop = mSmartspace.getTop();
474             post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout));
475         }
476 
477         if (mDisplayedClockSize != null && !mChildrenAreLaidOut) {
478             post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout));
479         }
480         mChildrenAreLaidOut = true;
481     }
482 
dump(PrintWriter pw, String[] args)483     public void dump(PrintWriter pw, String[] args) {
484         pw.println("KeyguardClockSwitch:");
485         pw.println("  mSmallClockFrame = " + mSmallClockFrame);
486         if (mSmallClockFrame != null) {
487             pw.println("  mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha());
488         }
489         pw.println("  mLargeClockFrame = " + mLargeClockFrame);
490         if (mLargeClockFrame != null) {
491             pw.println("  mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha());
492         }
493         pw.println("  mStatusArea = " + mStatusArea);
494         pw.println("  mDisplayedClockSize = " + mDisplayedClockSize);
495     }
496 }
497