/*
 * 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.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
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 KeyguardTransitionInteractor mKeyguardTransitionInteractor;
    private final DozeParameters mDozeParameters;

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

    private Boolean mSplitShadeEnabled = false;
    private Boolean mStatusViewCentered = true;
    private boolean mGoneToAodTransitionRunning = false;
    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,
            KeyguardTransitionInteractor keyguardTransitionInteractor,
            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;
        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
    }

    @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 (KeyguardShadeMigrationNssl.isEnabled()) {
            startCoroutines(EmptyCoroutineContext.INSTANCE);
        }
    }

    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);

        collectFlow(mView, mKeyguardTransitionInteractor.getGoneToAodTransition(),
                (TransitionStep step) -> {
                    if (step.getTransitionState() == TransitionState.RUNNING) {
                        mGoneToAodTransitionRunning = true;
                    } else {
                        mGoneToAodTransitionRunning = false;
                    }
                }, context);
    }

    public KeyguardStatusView getView() {
        return mView;
    }

    @Override
    protected void onViewAttached() {
        mStatusArea = mView.findViewById(R.id.keyguard_status_area);
        mStatusArea.addOnLayoutChangeListener(mStatusAreaLayoutChangeListener);
        mKeyguardUpdateMonitor.registerCallback(mInfoCallback);
        mConfigurationController.addCallback(mConfigurationListener);
    }

    @Override
    protected void onViewDetached() {
        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() && !mGoneToAodTransitionRunning) {
            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) {
        mKeyguardClockSwitchController.setSplitShadeCentered(splitShadeEnabled && shouldBeCentered);
        if (mStatusViewCentered == shouldBeCentered) {
            return;
        }

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

        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.clone(layout);
        int guideline;
        if (KeyguardShadeMigrationNssl.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();

        if (customClockAnimation) {
            // 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;
        }
    }
}
