/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.keyguard;

import static androidx.constraintlayout.widget.ConstraintSet.END;
import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID;

import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.transition.ChangeBounds;
import android.transition.Transition;
import android.transition.TransitionListenerAdapter;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.transition.TransitionValues;
import android.util.Slog;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.viewpager.widget.ViewPager;

import com.android.app.animation.Interpolators;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.keyguard.KeyguardClockSwitch.ClockSize;
import com.android.keyguard.logging.KeyguardLogger;
import com.android.systemui.Dumpable;
import com.android.systemui.animation.ViewHierarchyAnimator;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.plugins.clocks.ClockController;
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.power.shared.model.ScreenPowerState;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.notification.stack.AnimationProperties;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.ViewController;

import kotlin.coroutines.CoroutineContext;
import kotlin.coroutines.EmptyCoroutineContext;

import java.io.PrintWriter;

import javax.inject.Inject;

/**
 * Injectable controller for {@link KeyguardStatusView}.
 */
public class KeyguardStatusViewController extends ViewController<KeyguardStatusView> implements
        Dumpable {
    private static final boolean DEBUG = KeyguardConstants.DEBUG;
    @VisibleForTesting static final String TAG = "KeyguardStatusViewController";
    private static final long STATUS_AREA_HEIGHT_ANIMATION_MILLIS = 133;

    /**
     * Duration to use for the animator when the keyguard status view alignment changes, and a
     * custom clock animation is in use.
     */
    private static final int KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION = 1000;

    public static final AnimationProperties CLOCK_ANIMATION_PROPERTIES =
            new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);

    private final KeyguardSliceViewController mKeyguardSliceViewController;
    private final KeyguardClockSwitchController mKeyguardClockSwitchController;
    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
    private final ConfigurationController mConfigurationController;
    private final KeyguardVisibilityHelper mKeyguardVisibilityHelper;
    private final InteractionJankMonitor mInteractionJankMonitor;
    private final Rect mClipBounds = new Rect();
    private final KeyguardInteractor mKeyguardInteractor;
    private final PowerInteractor mPowerInteractor;
    private final DozeParameters mDozeParameters;

    private View mStatusArea = null;
    private ValueAnimator mStatusAreaHeightAnimator = null;

    private Boolean mSplitShadeEnabled = false;
    private Boolean mStatusViewCentered = true;
    private DumpManager mDumpManager;

    private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener =
            new TransitionListenerAdapter() {
                @Override
                public void onTransitionCancel(Transition transition) {
                    mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
                }

                @Override
                public void onTransitionEnd(Transition transition) {
                    mInteractionJankMonitor.end(CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
                }
            };

    private final View.OnLayoutChangeListener mStatusAreaLayoutChangeListener =
            new View.OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View v,
                        int left, int top, int right, int bottom,
                        int oldLeft, int oldTop, int oldRight, int oldBottom
                ) {
                    if (!mDozeParameters.getAlwaysOn()) {
                        return;
                    }

                    int oldHeight = oldBottom - oldTop;
                    int diff = v.getHeight() - oldHeight;
                    if (diff == 0) {
                        return;
                    }

                    int startValue = -1 * diff;
                    long duration = STATUS_AREA_HEIGHT_ANIMATION_MILLIS;
                    if (mStatusAreaHeightAnimator != null
                            && mStatusAreaHeightAnimator.isRunning()) {
                        duration += mStatusAreaHeightAnimator.getDuration()
                                - mStatusAreaHeightAnimator.getCurrentPlayTime();
                        startValue += (int) mStatusAreaHeightAnimator.getAnimatedValue();
                        mStatusAreaHeightAnimator.cancel();
                        mStatusAreaHeightAnimator = null;
                    }

                    mStatusAreaHeightAnimator = ValueAnimator.ofInt(startValue, 0);
                    mStatusAreaHeightAnimator.setDuration(duration);
                    final View nic = mKeyguardClockSwitchController.getAodNotifIconContainer();
                    if (nic != null) {
                        mStatusAreaHeightAnimator.addUpdateListener(anim -> {
                            nic.setTranslationY((int) anim.getAnimatedValue());
                        });
                    }
                    mStatusAreaHeightAnimator.start();
                }
            };

    @Inject
    public KeyguardStatusViewController(
            KeyguardStatusView keyguardStatusView,
            KeyguardSliceViewController keyguardSliceViewController,
            KeyguardClockSwitchController keyguardClockSwitchController,
            KeyguardStateController keyguardStateController,
            KeyguardUpdateMonitor keyguardUpdateMonitor,
            ConfigurationController configurationController,
            DozeParameters dozeParameters,
            ScreenOffAnimationController screenOffAnimationController,
            KeyguardLogger logger,
            InteractionJankMonitor interactionJankMonitor,
            KeyguardInteractor keyguardInteractor,
            DumpManager dumpManager,
            PowerInteractor powerInteractor) {
        super(keyguardStatusView);
        mKeyguardSliceViewController = keyguardSliceViewController;
        mKeyguardClockSwitchController = keyguardClockSwitchController;
        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
        mConfigurationController = configurationController;
        mDozeParameters = dozeParameters;
        mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
                dozeParameters, screenOffAnimationController, /* animateYPos= */ true,
                logger.getBuffer());
        mInteractionJankMonitor = interactionJankMonitor;
        mDumpManager = dumpManager;
        mKeyguardInteractor = keyguardInteractor;
        mPowerInteractor = powerInteractor;
    }

    @Override
    public void onInit() {
        mKeyguardClockSwitchController.init();
        final View mediaHostContainer = mView.findViewById(R.id.status_view_media_container);
        if (mediaHostContainer != null) {
            mKeyguardClockSwitchController.getView().addOnLayoutChangeListener(
                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                        if (!mSplitShadeEnabled
                                || mKeyguardClockSwitchController.getView().getSplitShadeCentered()
                                // Note: isKeyguardVisible() returns false after Launcher -> AOD.
                                || !mKeyguardUpdateMonitor.isKeyguardVisible()) {
                            return;
                        }

                        int oldHeight = oldBottom - oldTop;
                        if (v.getHeight() == oldHeight) return;

                        if (mediaHostContainer.getVisibility() != View.VISIBLE
                                // If the media is appearing, also don't do the transition.
                                || mediaHostContainer.getHeight() == 0) {
                            return;
                        }

                        ViewHierarchyAnimator.Companion.animateNextUpdate(mediaHostContainer,
                                Interpolators.STANDARD, /* duration= */ 500L,
                                /* animateChildren= */ false);
                    });
        }

        mDumpManager.registerDumpable(getInstanceName(), this);
        if (MigrateClocksToBlueprint.isEnabled()) {
            startCoroutines(EmptyCoroutineContext.INSTANCE);
            mView.setVisibility(View.GONE);
        }
    }

    void startCoroutines(CoroutineContext context) {
        collectFlow(mView, mKeyguardInteractor.getDozeTimeTick(),
                (Long millis) -> {
                        dozeTimeTick();
                }, context);

        collectFlow(mView, mPowerInteractor.getScreenPowerState(),
                (ScreenPowerState powerState) -> {
                    if (powerState == ScreenPowerState.SCREEN_TURNING_ON) {
                        dozeTimeTick();
                    }
                }, context);
    }

    public KeyguardStatusView getView() {
        return mView;
    }

    @Override
    protected void onViewAttached() {
        mStatusArea = mView.findViewById(R.id.keyguard_status_area);
        if (MigrateClocksToBlueprint.isEnabled()) {
            return;
        }

        mStatusArea.addOnLayoutChangeListener(mStatusAreaLayoutChangeListener);
        mKeyguardUpdateMonitor.registerCallback(mInfoCallback);
        mConfigurationController.addCallback(mConfigurationListener);
    }

    @Override
    protected void onViewDetached() {
        if (MigrateClocksToBlueprint.isEnabled()) {
            return;
        }

        mStatusArea.removeOnLayoutChangeListener(mStatusAreaLayoutChangeListener);
        mKeyguardUpdateMonitor.removeCallback(mInfoCallback);
        mConfigurationController.removeCallback(mConfigurationListener);
    }

    /** Sets the StatusView as shown on an external display. */
    public void setDisplayedOnSecondaryDisplay() {
        mKeyguardClockSwitchController.setShownOnSecondaryDisplay(true);
    }

    /**
     * Called in notificationPanelViewController to avoid leak
     */
    public void onDestroy() {
        mDumpManager.unregisterDumpable(getInstanceName());
    }

    /**
     * Updates views on doze time tick.
     */
    public void dozeTimeTick() {
        refreshTime();
        mKeyguardSliceViewController.refresh();
    }

    /**
     * Set which clock should be displayed on the keyguard. The other one will be automatically
     * hidden.
     */
    public void displayClock(@ClockSize int clockSize, boolean animate) {
        mKeyguardClockSwitchController.displayClock(clockSize, animate);
    }

    /**
     * Performs fold to aod animation of the clocks (changes font weight from bold to thin).
     * This animation is played when AOD is enabled and foldable device is fully folded, it is
     * displayed on the outer screen
     * @param foldFraction current fraction of fold animation complete
     */
    public void animateFoldToAod(float foldFraction) {
        mKeyguardClockSwitchController.animateFoldToAod(foldFraction);
    }

    /**
     * Sets a translationY on the views on the keyguard, except on the media view.
     */
    public void setTranslationY(float translationY, boolean excludeMedia) {
        mView.setChildrenTranslationY(translationY, excludeMedia);
    }

    /**
     * Set keyguard status view alpha.
     */
    public void setAlpha(float alpha) {
        if (!mKeyguardVisibilityHelper.isVisibilityAnimating()) {
            mView.setAlpha(alpha);
        }
    }

    /**
     * Update the pivot position based on the parent view
     */
    public void updatePivot(float parentWidth, float parentHeight) {
        mView.setPivotX(parentWidth / 2f);
        mView.setPivotY(mKeyguardClockSwitchController.getClockHeight() / 2f);
    }

    /**
     * Get the height of the keyguard status view without the notification icon area, as that's
     * only visible on AOD.
     *
     * We internally animate height changes to the status area to prevent discontinuities in the
     * doze animation introduced by the height suddenly changing due to smartpace.
     */
    public int getLockscreenHeight() {
        int heightAnimValue = mStatusAreaHeightAnimator == null ? 0 :
                (int) mStatusAreaHeightAnimator.getAnimatedValue();
        return mView.getHeight() + heightAnimValue
                - mKeyguardClockSwitchController.getNotificationIconAreaHeight();
    }

    /**
     * Get y-bottom position of the currently visible clock.
     */
    public int getClockBottom(int statusBarHeaderHeight) {
        return mKeyguardClockSwitchController.getClockBottom(statusBarHeaderHeight);
    }

    /**
     * @return true if the currently displayed clock is top aligned (as opposed to center aligned)
     */
    public boolean isClockTopAligned() {
        return mKeyguardClockSwitchController.isClockTopAligned();
    }

    /**
     * Pass top margin from ClockPositionAlgorithm in NotificationPanelViewController
     * Use for clock view in LS to compensate for top margin to align to the screen
     * Regardless of translation from AOD and unlock gestures
     */
    public void setLockscreenClockY(int clockY) {
        mKeyguardClockSwitchController.setLockscreenClockY(clockY);
    }

    /**
     * Set whether the view accessibility importance mode.
     */
    public void setStatusAccessibilityImportance(int mode) {
        mView.setImportantForAccessibility(mode);
    }

    @VisibleForTesting
    void setProperty(AnimatableProperty property, float value, boolean animate) {
        PropertyAnimator.setProperty(mView, property, value, CLOCK_ANIMATION_PROPERTIES, animate);
    }

    /**
     * Update position of the view with an optional animation
     */
    public void updatePosition(int x, int y, float scale, boolean animate) {
        setProperty(AnimatableProperty.Y, y, animate);

        ClockController clock = mKeyguardClockSwitchController.getClock();
        if (clock != null && clock.getConfig().getUseAlternateSmartspaceAODTransition()) {
            // If requested, scale the entire view instead of just the clock view
            mKeyguardClockSwitchController.updatePosition(x, 1f /* scale */,
                    CLOCK_ANIMATION_PROPERTIES, animate);
            setProperty(AnimatableProperty.SCALE_X, scale, animate);
            setProperty(AnimatableProperty.SCALE_Y, scale, animate);
        } else {
            mKeyguardClockSwitchController.updatePosition(x, scale,
                    CLOCK_ANIMATION_PROPERTIES, animate);
            setProperty(AnimatableProperty.SCALE_X, 1f, animate);
            setProperty(AnimatableProperty.SCALE_Y, 1f, animate);
        }
    }

    /**
     * Set the visibility of the keyguard status view based on some new state.
     */
    public void setKeyguardStatusViewVisibility(
            int statusBarState,
            boolean keyguardFadingAway,
            boolean goingToFullShade,
            int oldStatusBarState) {
        mKeyguardVisibilityHelper.setViewVisibility(
                statusBarState, keyguardFadingAway, goingToFullShade, oldStatusBarState);
    }

    private void refreshTime() {
        mKeyguardClockSwitchController.refresh();
    }

    private final ConfigurationController.ConfigurationListener mConfigurationListener =
            new ConfigurationController.ConfigurationListener() {
        @Override
        public void onLocaleListChanged() {
            refreshTime();
            mKeyguardClockSwitchController.onLocaleListChanged();
        }

        @Override
        public void onConfigChanged(Configuration newConfig) {
            mKeyguardClockSwitchController.onConfigChanged();
        }
    };

    private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
        @Override
        public void onTimeChanged() {
            Slog.v(TAG, "onTimeChanged");
            refreshTime();
        }

        @Override
        public void onKeyguardVisibilityChanged(boolean visible) {
            if (visible) {
                if (DEBUG) Slog.v(TAG, "refresh statusview visible:true");
                refreshTime();
            }
        }
    };

    /**
     * Rect that specifies how KSV should be clipped, on its parent's coordinates.
     */
    public void setClipBounds(Rect clipBounds) {
        if (clipBounds != null) {
            mClipBounds.set(clipBounds.left, (int) (clipBounds.top - mView.getY()),
                    clipBounds.right, (int) (clipBounds.bottom - mView.getY()));
            mView.setClipBounds(mClipBounds);
        } else {
            mView.setClipBounds(null);
        }
    }

    /**
     * Returns true if the large clock will block the notification shelf in AOD
     */
    public boolean isLargeClockBlockingNotificationShelf() {
        ClockController clock = mKeyguardClockSwitchController.getClock();
        return clock != null && clock.getLargeClock().getConfig().getHasCustomWeatherDataDisplay();
    }

    /**
     * Set if the split shade is enabled
     */
    public void setSplitShadeEnabled(boolean enabled) {
        mKeyguardClockSwitchController.setSplitShadeEnabled(enabled);
        mSplitShadeEnabled = enabled;
    }

    /**
     * Updates the alignment of the KeyguardStatusView and animates the transition if requested.
     */
    public void updateAlignment(
            ConstraintLayout layout,
            boolean splitShadeEnabled,
            boolean shouldBeCentered,
            boolean animate) {
        if (MigrateClocksToBlueprint.isEnabled()) {
            mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
        } else {
            mKeyguardClockSwitchController.setSplitShadeCentered(
                    splitShadeEnabled && shouldBeCentered);
        }
        if (mStatusViewCentered == shouldBeCentered) {
            return;
        }

        mStatusViewCentered = shouldBeCentered;
        if (layout == null) {
            return;
        }

        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.clone(layout);
        int guideline;
        if (MigrateClocksToBlueprint.isEnabled()) {
            guideline = R.id.split_shade_guideline;
        } else {
            guideline = R.id.qs_edge_guideline;
        }

        int statusConstraint = shouldBeCentered ? PARENT_ID : guideline;
        constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END);
        if (!animate) {
            constraintSet.applyTo(layout);
            return;
        }

        mInteractionJankMonitor.begin(mView, CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION);
        /* This transition blocks any layout changes while running. For that reason
        * special logic with setting visibility was added to {@link BcSmartspaceView#setDozing}
        * for split shade to avoid jump of the media object. */
        ChangeBounds transition = new ChangeBounds();
        if (splitShadeEnabled) {
            // Excluding media from the transition on split-shade, as it doesn't transition
            // horizontally properly.
            transition.excludeTarget(R.id.status_view_media_container, true);

            // Exclude smartspace viewpager and its children from the transition.
            //     - Each step of the transition causes the ViewPager to invoke resize,
            //     which invokes scrolling to the recalculated position. The scrolling
            //     actions are congested, resulting in kinky translation, and
            //     delay in settling to the final position. (http://b/281620564#comment1)
            //     - Also, the scrolling is unnecessary in the transition. We just want
            //     the viewpager to stay on the same page.
            //     - Exclude by Class type instead of resource id, since the resource id
            //     isn't available for all devices, and probably better to exclude all
            //     ViewPagers any way.
            transition.excludeTarget(ViewPager.class, true);
            transition.excludeChildren(ViewPager.class, true);
        }

        transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
        transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);

        ClockController clock = mKeyguardClockSwitchController.getClock();
        boolean customClockAnimation = clock != null
                && clock.getLargeClock().getConfig().getHasCustomPositionUpdatedAnimation();
        // When migrateClocksToBlueprint is on, customized clock animation is conducted in
        // KeyguardClockViewBinder
        if (customClockAnimation && !MigrateClocksToBlueprint.isEnabled()) {
            // Find the clock, so we can exclude it from this transition.
            FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large);

            // The clock container can sometimes be null. If it is, just fall back to the
            // old animation rather than setting up the custom animations.
            if (clockContainerView == null || clockContainerView.getChildCount() == 0) {
                transition.addListener(mKeyguardStatusAlignmentTransitionListener);
                TransitionManager.beginDelayedTransition(layout, transition);
            } else {
                View clockView = clockContainerView.getChildAt(0);

                TransitionSet set = new TransitionSet();
                set.addTransition(transition);

                SplitShadeTransitionAdapter adapter =
                        new SplitShadeTransitionAdapter(mKeyguardClockSwitchController);

                // Use linear here, so the actual clock can pick its own interpolator.
                adapter.setInterpolator(Interpolators.LINEAR);
                adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION);
                adapter.addTarget(clockView);
                set.addTransition(adapter);

                if (splitShadeEnabled) {
                    // Exclude smartspace viewpager and its children from the transition set.
                    //     - This is necessary in addition to excluding them from the
                    //     ChangeBounds child transition.
                    //     - Without this, the viewpager is scrolled to the new position
                    //     (corresponding to its end size) before the size change is realized.
                    //     Note that the size change is realized at the end of the ChangeBounds
                    //     transition. With the "prescrolling", the viewpager ends up in a weird
                    //     position, then recovers smoothly during the transition, and ends at
                    //     the position for the current page.
                    //     - Exclude by Class type instead of resource id, since the resource id
                    //     isn't available for all devices, and probably better to exclude all
                    //     ViewPagers any way.
                    set.excludeTarget(ViewPager.class, true);
                    set.excludeChildren(ViewPager.class, true);
                }

                set.addListener(mKeyguardStatusAlignmentTransitionListener);
                TransitionManager.beginDelayedTransition(layout, set);
            }
        } else {
            transition.addListener(mKeyguardStatusAlignmentTransitionListener);
            TransitionManager.beginDelayedTransition(layout, transition);
        }

        constraintSet.applyTo(layout);
    }

    public ClockController getClockController() {
        return mKeyguardClockSwitchController.getClock();
    }

    @Override
    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
        mView.dump(pw, args);
    }

    String getInstanceName() {
        return TAG + "#" + hashCode();
    }

    @VisibleForTesting
    static class SplitShadeTransitionAdapter extends Transition {
        private static final String PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft";
        private static final String PROP_BOUNDS_RIGHT = "splitShadeTransitionAdapter:boundsRight";
        private static final String PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow";
        private static final String[] TRANSITION_PROPERTIES = {
                PROP_BOUNDS_LEFT, PROP_BOUNDS_RIGHT, PROP_X_IN_WINDOW};

        private final KeyguardClockSwitchController mController;

        @VisibleForTesting
        SplitShadeTransitionAdapter(KeyguardClockSwitchController controller) {
            mController = controller;
        }

        private void captureValues(TransitionValues transitionValues) {
            transitionValues.values.put(PROP_BOUNDS_LEFT, transitionValues.view.getLeft());
            transitionValues.values.put(PROP_BOUNDS_RIGHT, transitionValues.view.getRight());
            int[] locationInWindowTmp = new int[2];
            transitionValues.view.getLocationInWindow(locationInWindowTmp);
            transitionValues.values.put(PROP_X_IN_WINDOW, locationInWindowTmp[0]);
        }

        @Override
        public void captureEndValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }

        @Override
        public void captureStartValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }

        @Nullable
        @Override
        public Animator createAnimator(@NonNull ViewGroup sceneRoot,
                @Nullable TransitionValues startValues,
                @Nullable TransitionValues endValues) {
            if (startValues == null || endValues == null) {
                return null;
            }
            ValueAnimator anim = ValueAnimator.ofFloat(0, 1);

            int fromLeft = (int) startValues.values.get(PROP_BOUNDS_LEFT);
            int fromWindowX = (int) startValues.values.get(PROP_X_IN_WINDOW);
            int toWindowX = (int) endValues.values.get(PROP_X_IN_WINDOW);
            // Using windowX, to determine direction, instead of left, as in RTL the difference of
            // toLeft - fromLeft is always positive, even when moving left.
            int direction = toWindowX - fromWindowX > 0 ? 1 : -1;

            anim.addUpdateListener(animation -> {
                ClockController clock = mController.getClock();
                if (clock == null) {
                    return;
                }

                clock.getLargeClock().getAnimations()
                        .onPositionUpdated(fromLeft, direction, animation.getAnimatedFraction());
            });

            return anim;
        }

        @Override
        public String[] getTransitionProperties() {
            return TRANSITION_PROPERTIES;
        }
    }
}
