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