1 /*
2  * Copyright (C) 2020 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 
17 package com.android.keyguard;
18 
19 import static androidx.constraintlayout.widget.ConstraintSet.END;
20 import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;
21 
22 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION;
23 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
24 
25 import android.animation.Animator;
26 import android.animation.ValueAnimator;
27 import android.annotation.Nullable;
28 import android.content.res.Configuration;
29 import android.graphics.Rect;
30 import android.transition.ChangeBounds;
31 import android.transition.Transition;
32 import android.transition.TransitionListenerAdapter;
33 import android.transition.TransitionManager;
34 import android.transition.TransitionSet;
35 import android.transition.TransitionValues;
36 import android.util.Slog;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.widget.FrameLayout;
40 
41 import androidx.annotation.NonNull;
42 import androidx.annotation.VisibleForTesting;
43 import androidx.constraintlayout.widget.ConstraintLayout;
44 import androidx.constraintlayout.widget.ConstraintSet;
45 import androidx.viewpager.widget.ViewPager;
46 
47 import com.android.app.animation.Interpolators;
48 import com.android.internal.jank.InteractionJankMonitor;
49 import com.android.keyguard.KeyguardClockSwitch.ClockSize;
50 import com.android.keyguard.logging.KeyguardLogger;
51 import com.android.systemui.Dumpable;
52 import com.android.systemui.R;
53 import com.android.systemui.dump.DumpManager;
54 import com.android.systemui.flags.FeatureFlags;
55 import com.android.systemui.flags.Flags;
56 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
57 import com.android.systemui.keyguard.shared.model.ScreenModel;
58 import com.android.systemui.keyguard.shared.model.ScreenState;
59 import com.android.systemui.plugins.ClockController;
60 import com.android.systemui.statusbar.notification.AnimatableProperty;
61 import com.android.systemui.statusbar.notification.PropertyAnimator;
62 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
63 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
64 import com.android.systemui.statusbar.phone.DozeParameters;
65 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
66 import com.android.systemui.statusbar.policy.ConfigurationController;
67 import com.android.systemui.statusbar.policy.KeyguardStateController;
68 import com.android.systemui.util.ViewController;
69 
70 import kotlin.coroutines.CoroutineContext;
71 import kotlin.coroutines.EmptyCoroutineContext;
72 
73 import java.io.PrintWriter;
74 
75 import javax.inject.Inject;
76 
77 /**
78  * Injectable controller for {@link KeyguardStatusView}.
79  */
80 public class KeyguardStatusViewController extends ViewController<KeyguardStatusView> implements
81         Dumpable {
82     private static final boolean DEBUG = KeyguardConstants.DEBUG;
83     @VisibleForTesting static final String TAG = "KeyguardStatusViewController";
84 
85     /**
86      * Duration to use for the animator when the keyguard status view alignment changes, and a
87      * custom clock animation is in use.
88      */
89     private static final int KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION = 1000;
90 
91     public static final AnimationProperties CLOCK_ANIMATION_PROPERTIES =
92             new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
93 
94     private final KeyguardSliceViewController mKeyguardSliceViewController;
95     private final KeyguardClockSwitchController mKeyguardClockSwitchController;
96     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
97     private final ConfigurationController mConfigurationController;
98     private final KeyguardVisibilityHelper mKeyguardVisibilityHelper;
99     private final FeatureFlags mFeatureFlags;
100     private final InteractionJankMonitor mInteractionJankMonitor;
101     private final Rect mClipBounds = new Rect();
102     private final KeyguardInteractor mKeyguardInteractor;
103 
104     private Boolean mStatusViewCentered = true;
105 
106     private DumpManager mDumpManager;
107 
108     private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener =
109             new TransitionListenerAdapter() {
110                 @Override
111                 public void onTransitionCancel(Transition transition) {
112                     mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
113                 }
114 
115                 @Override
116                 public void onTransitionEnd(Transition transition) {
117                     mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
118                 }
119             };
120 
121     @Inject
KeyguardStatusViewController( KeyguardStatusView keyguardStatusView, KeyguardSliceViewController keyguardSliceViewController, KeyguardClockSwitchController keyguardClockSwitchController, KeyguardStateController keyguardStateController, KeyguardUpdateMonitor keyguardUpdateMonitor, ConfigurationController configurationController, DozeParameters dozeParameters, ScreenOffAnimationController screenOffAnimationController, KeyguardLogger logger, FeatureFlags featureFlags, InteractionJankMonitor interactionJankMonitor, KeyguardInteractor keyguardInteractor, DumpManager dumpManager)122     public KeyguardStatusViewController(
123             KeyguardStatusView keyguardStatusView,
124             KeyguardSliceViewController keyguardSliceViewController,
125             KeyguardClockSwitchController keyguardClockSwitchController,
126             KeyguardStateController keyguardStateController,
127             KeyguardUpdateMonitor keyguardUpdateMonitor,
128             ConfigurationController configurationController,
129             DozeParameters dozeParameters,
130             ScreenOffAnimationController screenOffAnimationController,
131             KeyguardLogger logger,
132             FeatureFlags featureFlags,
133             InteractionJankMonitor interactionJankMonitor,
134             KeyguardInteractor keyguardInteractor,
135             DumpManager dumpManager) {
136         super(keyguardStatusView);
137         mKeyguardSliceViewController = keyguardSliceViewController;
138         mKeyguardClockSwitchController = keyguardClockSwitchController;
139         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
140         mConfigurationController = configurationController;
141         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
142                 dozeParameters, screenOffAnimationController, /* animateYPos= */ true,
143                 logger.getBuffer());
144         mInteractionJankMonitor = interactionJankMonitor;
145         mFeatureFlags = featureFlags;
146         mDumpManager = dumpManager;
147         mKeyguardInteractor = keyguardInteractor;
148     }
149 
150     @Override
onInit()151     public void onInit() {
152         mKeyguardClockSwitchController.init();
153 
154         mDumpManager.registerDumpable(getInstanceName(), this);
155         if (mFeatureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW)) {
156             startCoroutines(EmptyCoroutineContext.INSTANCE);
157         }
158     }
159 
startCoroutines(CoroutineContext context)160     void startCoroutines(CoroutineContext context) {
161         collectFlow(mView, mKeyguardInteractor.getDozeTimeTick(),
162                 (Long millis) -> {
163                         dozeTimeTick();
164                 }, context);
165 
166         collectFlow(mView, mKeyguardInteractor.getScreenModel(),
167                 (ScreenModel model) -> {
168                     if (model.getState() == ScreenState.SCREEN_TURNING_ON) {
169                         dozeTimeTick();
170                     }
171                 }, context);
172     }
173 
getView()174     public KeyguardStatusView getView() {
175         return mView;
176     }
177 
178     @Override
onViewAttached()179     protected void onViewAttached() {
180         mKeyguardUpdateMonitor.registerCallback(mInfoCallback);
181         mConfigurationController.addCallback(mConfigurationListener);
182     }
183 
184     @Override
onViewDetached()185     protected void onViewDetached() {
186         mKeyguardUpdateMonitor.removeCallback(mInfoCallback);
187         mConfigurationController.removeCallback(mConfigurationListener);
188     }
189 
190     /** Sets the StatusView as shown on an external display. */
setDisplayedOnSecondaryDisplay()191     public void setDisplayedOnSecondaryDisplay() {
192         mKeyguardClockSwitchController.setShownOnSecondaryDisplay(true);
193     }
194 
195     /**
196      * Called in notificationPanelViewController to avoid leak
197      */
onDestroy()198     public void onDestroy() {
199         mDumpManager.unregisterDumpable(getInstanceName());
200     }
201 
202     /**
203      * Updates views on doze time tick.
204      */
dozeTimeTick()205     public void dozeTimeTick() {
206         refreshTime();
207         mKeyguardSliceViewController.refresh();
208     }
209 
210     /**
211      * Set which clock should be displayed on the keyguard. The other one will be automatically
212      * hidden.
213      */
displayClock(@lockSize int clockSize, boolean animate)214     public void displayClock(@ClockSize int clockSize, boolean animate) {
215         mKeyguardClockSwitchController.displayClock(clockSize, animate);
216     }
217 
218     /**
219      * Performs fold to aod animation of the clocks (changes font weight from bold to thin).
220      * This animation is played when AOD is enabled and foldable device is fully folded, it is
221      * displayed on the outer screen
222      * @param foldFraction current fraction of fold animation complete
223      */
animateFoldToAod(float foldFraction)224     public void animateFoldToAod(float foldFraction) {
225         mKeyguardClockSwitchController.animateFoldToAod(foldFraction);
226     }
227 
228     /**
229      * Sets a translationY on the views on the keyguard, except on the media view.
230      */
setTranslationY(float translationY, boolean excludeMedia)231     public void setTranslationY(float translationY, boolean excludeMedia) {
232         mView.setChildrenTranslationY(translationY, excludeMedia);
233     }
234 
235     /**
236      * Set keyguard status view alpha.
237      */
setAlpha(float alpha)238     public void setAlpha(float alpha) {
239         if (!mKeyguardVisibilityHelper.isVisibilityAnimating()) {
240             mView.setAlpha(alpha);
241         }
242     }
243 
244     /**
245      * Update the pivot position based on the parent view
246      */
updatePivot(float parentWidth, float parentHeight)247     public void updatePivot(float parentWidth, float parentHeight) {
248         mView.setPivotX(parentWidth / 2f);
249         mView.setPivotY(mKeyguardClockSwitchController.getClockHeight() / 2f);
250     }
251 
252     /**
253      * Get the height of the keyguard status view without the notification icon area, as that's
254      * only visible on AOD.
255      */
getLockscreenHeight()256     public int getLockscreenHeight() {
257         return mView.getHeight() - mKeyguardClockSwitchController.getNotificationIconAreaHeight();
258     }
259 
260     /**
261      * Get y-bottom position of the currently visible clock.
262      */
getClockBottom(int statusBarHeaderHeight)263     public int getClockBottom(int statusBarHeaderHeight) {
264         return mKeyguardClockSwitchController.getClockBottom(statusBarHeaderHeight);
265     }
266 
267     /**
268      * @return true if the currently displayed clock is top aligned (as opposed to center aligned)
269      */
isClockTopAligned()270     public boolean isClockTopAligned() {
271         return mKeyguardClockSwitchController.isClockTopAligned();
272     }
273 
274     /**
275      * Pass top margin from ClockPositionAlgorithm in NotificationPanelViewController
276      * Use for clock view in LS to compensate for top margin to align to the screen
277      * Regardless of translation from AOD and unlock gestures
278      */
setLockscreenClockY(int clockY)279     public void setLockscreenClockY(int clockY) {
280         mKeyguardClockSwitchController.setLockscreenClockY(clockY);
281     }
282 
283     /**
284      * Set whether the view accessibility importance mode.
285      */
setStatusAccessibilityImportance(int mode)286     public void setStatusAccessibilityImportance(int mode) {
287         mView.setImportantForAccessibility(mode);
288     }
289 
290     @VisibleForTesting
setProperty(AnimatableProperty property, float value, boolean animate)291     void setProperty(AnimatableProperty property, float value, boolean animate) {
292         PropertyAnimator.setProperty(mView, property, value, CLOCK_ANIMATION_PROPERTIES, animate);
293     }
294 
295     /**
296      * Update position of the view with an optional animation
297      */
updatePosition(int x, int y, float scale, boolean animate)298     public void updatePosition(int x, int y, float scale, boolean animate) {
299         setProperty(AnimatableProperty.Y, y, animate);
300 
301         ClockController clock = mKeyguardClockSwitchController.getClock();
302         if (clock != null && clock.getConfig().getUseAlternateSmartspaceAODTransition()) {
303             // If requested, scale the entire view instead of just the clock view
304             mKeyguardClockSwitchController.updatePosition(x, 1f /* scale */,
305                     CLOCK_ANIMATION_PROPERTIES, animate);
306             setProperty(AnimatableProperty.SCALE_X, scale, animate);
307             setProperty(AnimatableProperty.SCALE_Y, scale, animate);
308         } else {
309             mKeyguardClockSwitchController.updatePosition(x, scale,
310                     CLOCK_ANIMATION_PROPERTIES, animate);
311             setProperty(AnimatableProperty.SCALE_X, 1f, animate);
312             setProperty(AnimatableProperty.SCALE_Y, 1f, animate);
313         }
314     }
315 
316     /**
317      * Set the visibility of the keyguard status view based on some new state.
318      */
setKeyguardStatusViewVisibility( int statusBarState, boolean keyguardFadingAway, boolean goingToFullShade, int oldStatusBarState)319     public void setKeyguardStatusViewVisibility(
320             int statusBarState,
321             boolean keyguardFadingAway,
322             boolean goingToFullShade,
323             int oldStatusBarState) {
324         mKeyguardVisibilityHelper.setViewVisibility(
325                 statusBarState, keyguardFadingAway, goingToFullShade, oldStatusBarState);
326     }
327 
refreshTime()328     private void refreshTime() {
329         mKeyguardClockSwitchController.refresh();
330     }
331 
332     private final ConfigurationController.ConfigurationListener mConfigurationListener =
333             new ConfigurationController.ConfigurationListener() {
334         @Override
335         public void onLocaleListChanged() {
336             refreshTime();
337             mKeyguardClockSwitchController.onLocaleListChanged();
338         }
339 
340         @Override
341         public void onConfigChanged(Configuration newConfig) {
342             mKeyguardClockSwitchController.onConfigChanged();
343         }
344     };
345 
346     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
347         @Override
348         public void onTimeChanged() {
349             Slog.v(TAG, "onTimeChanged");
350             refreshTime();
351         }
352 
353         @Override
354         public void onKeyguardVisibilityChanged(boolean visible) {
355             if (visible) {
356                 if (DEBUG) Slog.v(TAG, "refresh statusview visible:true");
357                 refreshTime();
358             }
359         }
360     };
361 
362     /**
363      * Rect that specifies how KSV should be clipped, on its parent's coordinates.
364      */
setClipBounds(Rect clipBounds)365     public void setClipBounds(Rect clipBounds) {
366         if (clipBounds != null) {
367             mClipBounds.set(clipBounds.left, (int) (clipBounds.top - mView.getY()),
368                     clipBounds.right, (int) (clipBounds.bottom - mView.getY()));
369             mView.setClipBounds(mClipBounds);
370         } else {
371             mView.setClipBounds(null);
372         }
373     }
374 
375     /**
376      * Returns true if the large clock will block the notification shelf in AOD
377      */
isLargeClockBlockingNotificationShelf()378     public boolean isLargeClockBlockingNotificationShelf() {
379         ClockController clock = mKeyguardClockSwitchController.getClock();
380         return clock != null && clock.getLargeClock().getConfig().getHasCustomWeatherDataDisplay();
381     }
382 
383     /**
384      * Set if the split shade is enabled
385      */
setSplitShadeEnabled(boolean enabled)386     public void setSplitShadeEnabled(boolean enabled) {
387         mKeyguardClockSwitchController.setSplitShadeEnabled(enabled);
388     }
389 
390     /**
391      * Updates the alignment of the KeyguardStatusView and animates the transition if requested.
392      */
updateAlignment( ConstraintLayout layout, boolean splitShadeEnabled, boolean shouldBeCentered, boolean animate)393     public void updateAlignment(
394             ConstraintLayout layout,
395             boolean splitShadeEnabled,
396             boolean shouldBeCentered,
397             boolean animate) {
398         mKeyguardClockSwitchController.setSplitShadeCentered(splitShadeEnabled && shouldBeCentered);
399         if (mStatusViewCentered == shouldBeCentered) {
400             return;
401         }
402 
403         mStatusViewCentered = shouldBeCentered;
404         if (layout == null) {
405             return;
406         }
407 
408         ConstraintSet constraintSet = new ConstraintSet();
409         constraintSet.clone(layout);
410         int guideline;
411         if (mFeatureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW)) {
412             guideline = R.id.split_shade_guideline;
413         } else {
414             guideline = R.id.qs_edge_guideline;
415         }
416 
417         int statusConstraint = shouldBeCentered ? PARENT_ID : guideline;
418         constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END);
419         if (!animate) {
420             constraintSet.applyTo(layout);
421             return;
422         }
423 
424         mInteractionJankMonitor.begin(mView, CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
425         /* This transition blocks any layout changes while running. For that reason
426         * special logic with setting visibility was added to {@link BcSmartspaceView#setDozing}
427         * for split shade to avoid jump of the media object. */
428         ChangeBounds transition = new ChangeBounds();
429         if (splitShadeEnabled) {
430             // Excluding media from the transition on split-shade, as it doesn't transition
431             // horizontally properly.
432             transition.excludeTarget(R.id.status_view_media_container, true);
433 
434             // Exclude smartspace viewpager and its children from the transition.
435             //     - Each step of the transition causes the ViewPager to invoke resize,
436             //     which invokes scrolling to the recalculated position. The scrolling
437             //     actions are congested, resulting in kinky translation, and
438             //     delay in settling to the final position. (http://b/281620564#comment1)
439             //     - Also, the scrolling is unnecessary in the transition. We just want
440             //     the viewpager to stay on the same page.
441             //     - Exclude by Class type instead of resource id, since the resource id
442             //     isn't available for all devices, and probably better to exclude all
443             //     ViewPagers any way.
444             transition.excludeTarget(ViewPager.class, true);
445             transition.excludeChildren(ViewPager.class, true);
446         }
447 
448         transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
449         transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
450 
451         ClockController clock = mKeyguardClockSwitchController.getClock();
452         boolean customClockAnimation = clock != null
453                 && clock.getLargeClock().getConfig().getHasCustomPositionUpdatedAnimation();
454 
455         if (customClockAnimation) {
456             // Find the clock, so we can exclude it from this transition.
457             FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large);
458 
459             // The clock container can sometimes be null. If it is, just fall back to the
460             // old animation rather than setting up the custom animations.
461             if (clockContainerView == null || clockContainerView.getChildCount() == 0) {
462                 transition.addListener(mKeyguardStatusAlignmentTransitionListener);
463                 TransitionManager.beginDelayedTransition(layout, transition);
464             } else {
465                 View clockView = clockContainerView.getChildAt(0);
466 
467                 TransitionSet set = new TransitionSet();
468                 set.addTransition(transition);
469 
470                 SplitShadeTransitionAdapter adapter =
471                         new SplitShadeTransitionAdapter(mKeyguardClockSwitchController);
472 
473                 // Use linear here, so the actual clock can pick its own interpolator.
474                 adapter.setInterpolator(Interpolators.LINEAR);
475                 adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION);
476                 adapter.addTarget(clockView);
477                 set.addTransition(adapter);
478 
479                 if (splitShadeEnabled) {
480                     // Exclude smartspace viewpager and its children from the transition set.
481                     //     - This is necessary in addition to excluding them from the
482                     //     ChangeBounds child transition.
483                     //     - Without this, the viewpager is scrolled to the new position
484                     //     (corresponding to its end size) before the size change is realized.
485                     //     Note that the size change is realized at the end of the ChangeBounds
486                     //     transition. With the "prescrolling", the viewpager ends up in a weird
487                     //     position, then recovers smoothly during the transition, and ends at
488                     //     the position for the current page.
489                     //     - Exclude by Class type instead of resource id, since the resource id
490                     //     isn't available for all devices, and probably better to exclude all
491                     //     ViewPagers any way.
492                     set.excludeTarget(ViewPager.class, true);
493                     set.excludeChildren(ViewPager.class, true);
494                 }
495 
496                 set.addListener(mKeyguardStatusAlignmentTransitionListener);
497                 TransitionManager.beginDelayedTransition(layout, set);
498             }
499         } else {
500             transition.addListener(mKeyguardStatusAlignmentTransitionListener);
501             TransitionManager.beginDelayedTransition(layout, transition);
502         }
503 
504         constraintSet.applyTo(layout);
505     }
506 
507     @Override
dump(@onNull PrintWriter pw, @NonNull String[] args)508     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
509         mView.dump(pw, args);
510     }
511 
getInstanceName()512     String getInstanceName() {
513         return TAG + "#" + hashCode();
514     }
515 
516     @VisibleForTesting
517     static class SplitShadeTransitionAdapter extends Transition {
518         private static final String PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft";
519         private static final String PROP_BOUNDS_RIGHT = "splitShadeTransitionAdapter:boundsRight";
520         private static final String PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow";
521         private static final String[] TRANSITION_PROPERTIES = {
522                 PROP_BOUNDS_LEFT, PROP_BOUNDS_RIGHT, PROP_X_IN_WINDOW};
523 
524         private final KeyguardClockSwitchController mController;
525 
526         @VisibleForTesting
SplitShadeTransitionAdapter(KeyguardClockSwitchController controller)527         SplitShadeTransitionAdapter(KeyguardClockSwitchController controller) {
528             mController = controller;
529         }
530 
captureValues(TransitionValues transitionValues)531         private void captureValues(TransitionValues transitionValues) {
532             transitionValues.values.put(PROP_BOUNDS_LEFT, transitionValues.view.getLeft());
533             transitionValues.values.put(PROP_BOUNDS_RIGHT, transitionValues.view.getRight());
534             int[] locationInWindowTmp = new int[2];
535             transitionValues.view.getLocationInWindow(locationInWindowTmp);
536             transitionValues.values.put(PROP_X_IN_WINDOW, locationInWindowTmp[0]);
537         }
538 
539         @Override
captureEndValues(TransitionValues transitionValues)540         public void captureEndValues(TransitionValues transitionValues) {
541             captureValues(transitionValues);
542         }
543 
544         @Override
captureStartValues(TransitionValues transitionValues)545         public void captureStartValues(TransitionValues transitionValues) {
546             captureValues(transitionValues);
547         }
548 
549         @Nullable
550         @Override
createAnimator(@onNull ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues)551         public Animator createAnimator(@NonNull ViewGroup sceneRoot,
552                 @Nullable TransitionValues startValues,
553                 @Nullable TransitionValues endValues) {
554             if (startValues == null || endValues == null) {
555                 return null;
556             }
557             ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
558 
559             int fromLeft = (int) startValues.values.get(PROP_BOUNDS_LEFT);
560             int fromWindowX = (int) startValues.values.get(PROP_X_IN_WINDOW);
561             int toWindowX = (int) endValues.values.get(PROP_X_IN_WINDOW);
562             // Using windowX, to determine direction, instead of left, as in RTL the difference of
563             // toLeft - fromLeft is always positive, even when moving left.
564             int direction = toWindowX - fromWindowX > 0 ? 1 : -1;
565 
566             anim.addUpdateListener(animation -> {
567                 ClockController clock = mController.getClock();
568                 if (clock == null) {
569                     return;
570                 }
571 
572                 clock.getLargeClock().getAnimations()
573                         .onPositionUpdated(fromLeft, direction, animation.getAnimatedFraction());
574             });
575 
576             return anim;
577         }
578 
579         @Override
getTransitionProperties()580         public String[] getTransitionProperties() {
581             return TRANSITION_PROPERTIES;
582         }
583     }
584 }
585