Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3228688a authored by Android Build Coastguard Worker's avatar Android Build Coastguard Worker
Browse files

Merge cherrypicks of ['googleplex-android-review.googlesource.com/35388160',...

Merge cherrypicks of ['googleplex-android-review.googlesource.com/35388160', 'googleplex-android-review.googlesource.com/35741041'] into 25Q4-release.

Change-Id: I61a253255d2d7cc4accbbd653d22e65e1229bc33
parents 2a3b0287 fd04c7ff
Loading
Loading
Loading
Loading
+105 −3
Original line number Diff line number Diff line
@@ -19,15 +19,22 @@ import android.animation.AnimatorTestRule
import android.util.FloatProperty
import android.util.Property
import android.view.View

import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.dynamicanimation.animation.DynamicAnimation
import com.android.internal.dynamicanimation.animation.SpringForce
import com.android.systemui.SysuiTestCase
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.stack.AnimationProperties
import com.android.systemui.statusbar.notification.stack.ViewState
import com.google.common.truth.Truth.assertThat
import kotlin.math.PI
import kotlin.math.ceil
import kotlin.math.exp
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.sqrt
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
@@ -53,8 +60,7 @@ class PhysicsPropertyAnimatorTest : SysuiTestCase() {
                return _value
            }
        }
    @get:Rule
    val animatorTestRule = AnimatorTestRule(this)
    @get:Rule val animatorTestRule = AnimatorTestRule(this)
    private val property: PhysicsProperty =
        PhysicsProperty(R.id.scale_x_animator_tag, effectiveProperty)
    private var finishListener: DynamicAnimation.OnAnimationEndListener? = null
@@ -269,4 +275,100 @@ class PhysicsPropertyAnimatorTest : SysuiTestCase() {
        propertyData.animator?.cancel()
        Mockito.verify(finishListener2).onAnimationEnd(any(), any(), any(), any())
    }

    @Test
    fun limitOvershoot_zeroInitialDistance_doesNotModifySpring() {
        val underTest = createTestSpring()

        underTest.limitOvershoot(initialDisplacement = 0f, maxOvershoot = 100f)
        assertThat(underTest.stiffness).isEqualTo(STIFFNESS_TEST)
        assertThat(underTest.dampingRatio).isEqualTo(DAMPING_RATIO_TEST)
    }

    @Test
    fun limitOvershoot_initialDistanceLessThanMaxOvershoot_doesNotModifySpring() {
        val underTest = createTestSpring()

        underTest.limitOvershoot(initialDisplacement = 50f, maxOvershoot = 100f)
        assertThat(underTest.stiffness).isEqualTo(STIFFNESS_TEST)
        assertThat(underTest.dampingRatio).isEqualTo(DAMPING_RATIO_TEST)
    }

    @Test
    fun limitOvershoot_initialDistanceLessThanMaxOvershoot_negativeValue_doesNotModifySpring() {
        val underTest = createTestSpring()

        underTest.limitOvershoot(initialDisplacement = -50f, maxOvershoot = 100f)
        assertThat(underTest.stiffness).isEqualTo(STIFFNESS_TEST)
        assertThat(underTest.dampingRatio).isEqualTo(DAMPING_RATIO_TEST)
    }

    @Test
    fun limitOvershoot_initialDistanceDoesNotExceedMaxOvershoot_doesNotModifySpring() {
        val underTest = createTestSpring()

        // roughly ~5.43 in this scenario
        val maxOvershoot = calculateMaxOvershoot(DAMPING_RATIO_TEST, initialDisplacement = 100f)
        underTest.limitOvershoot(initialDisplacement = 100f, maxOvershoot = ceil(maxOvershoot))

        assertThat(underTest.stiffness).isEqualTo(STIFFNESS_TEST)
        assertThat(underTest.dampingRatio).isEqualTo(DAMPING_RATIO_TEST)
    }

    @Test
    fun limitOvershoot_initialDistanceDoesExceedMaxOvershoot_modifiedSpringAndStiffness() {
        val underTest = createTestSpring()

        // roughly ~5.43 in this scenario
        val maxOvershoot = calculateMaxOvershoot(DAMPING_RATIO_TEST, initialDisplacement = 100f)
        underTest.limitOvershoot(initialDisplacement = 100f, maxOvershoot = floor(maxOvershoot))

        assertThat(underTest.stiffness).isGreaterThan(STIFFNESS_TEST)
        assertThat(underTest.dampingRatio).isGreaterThan(DAMPING_RATIO_TEST)
    }

    @Test
    fun limitOvershoot_whenConstrained_newParametersAreCorrect() {
        val underTest = createTestSpring()

        val maxOvershoot =
            floor(calculateMaxOvershoot(DAMPING_RATIO_TEST, initialDisplacement = 100f))

        underTest.limitOvershoot(initialDisplacement = 100f, maxOvershoot = maxOvershoot)

        val dampingDisplacement100 = underTest.dampingRatio

        underTest.limitOvershoot(initialDisplacement = 1000f, maxOvershoot = maxOvershoot)

        val dampingDisplacement1000 = underTest.dampingRatio

        // Damping must be further constrained for a bigger initial displacement
        assertThat(dampingDisplacement1000).isGreaterThan(dampingDisplacement100)

        assertThat(calculateMaxOvershoot(dampingDisplacement1000, 1000f))
            .isWithin(0.1f)
            .of(maxOvershoot)
    }

    companion object {
        const val STIFFNESS_TEST = 380f
        const val DAMPING_RATIO_TEST = .68f

        fun createTestSpring() =
            SpringForce().setStiffness(STIFFNESS_TEST).setDampingRatio(DAMPING_RATIO_TEST)

        fun calculateMaxOvershoot(dampingRatio: Float, initialDisplacement: Float): Float {
            require(dampingRatio > 0 && dampingRatio < 1)

            val zeta = dampingRatio.toDouble()

            val numerator = -(zeta * PI)
            val denominator = sqrt(1.0 - zeta.pow(2))
            val exponent = numerator / denominator

            val overshootRatio = exp(exponent)

            return (initialDisplacement * overshootRatio).toFloat()
        }
    }
}
+138 −24
Original line number Diff line number Diff line
@@ -16,15 +16,24 @@
package com.android.systemui.statusbar.notification

import android.util.FloatProperty
import android.util.Log
import android.util.Property
import android.view.View
import androidx.annotation.VisibleForTesting
import com.android.internal.dynamicanimation.animation.DynamicAnimation
import com.android.internal.dynamicanimation.animation.SpringAnimation
import com.android.internal.dynamicanimation.animation.SpringForce
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Companion.TAG
import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Companion.createDefaultSpring
import com.android.systemui.statusbar.notification.stack.AnimationProperties
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.asin
import kotlin.math.ln
import kotlin.math.pow
import kotlin.math.sign
import kotlin.math.sqrt

/**
 * A physically animatable property of a view.
@@ -34,8 +43,11 @@ import kotlin.math.sign
 * @param avoidDoubleOvershoot should this property avoid double overshoot when animated
 */
data class PhysicsProperty
@JvmOverloads constructor(
    val tag: Int, val property: Property<View, Float>, val avoidDoubleOvershoot: Boolean = true
@JvmOverloads
constructor(
    val tag: Int,
    val property: Property<View, Float>,
    val avoidDoubleOvershoot: Boolean = true,
) {
    val offsetProperty =
        object : FloatProperty<View>(property.name) {
@@ -67,12 +79,12 @@ data class PropertyData(
    var delayRunnable: Runnable? = null,

    /**
     * A runnable that should be executed if the animation is skipped to end / cancelled before
     * the animation actually starts running.
     * A runnable that should be executed if the animation is skipped to end / cancelled before the
     * animation actually starts running.
     */
    var endedBeforeStartingCleanupHandler: ((Boolean) -> Unit)? = null,
    var startOffset: Float = 0f,
    var doubleOvershootAvoidingListener: DynamicAnimation.OnAnimationUpdateListener? = null
    var doubleOvershootAvoidingListener: DynamicAnimation.OnAnimationUpdateListener? = null,
)

/**
@@ -99,9 +111,7 @@ class PhysicsPropertyAnimator {
        // Uses the standard spatial material spring by default
        @JvmStatic
        fun createDefaultSpring(): SpringForce {
            return SpringForce()
                .setStiffness(380f)
                .setDampingRatio(0.68f);
            return SpringForce().setStiffness(380f).setDampingRatio(0.68f)
        }

        @JvmStatic
@@ -111,6 +121,9 @@ class PhysicsPropertyAnimator {
         * animated can be used to request an animation. If the view isn't animated, this utility
         * will update the current animation if existent, such that the end value will point
         * to @param newEndValue or apply it directly if there's no animation.
         *
         * @param maxOvershoot limit the spring overshoot of the animation. If specified, must be a
         *   positive, finite distance.
         */
        fun setProperty(
            view: View,
@@ -119,9 +132,17 @@ class PhysicsPropertyAnimator {
            properties: AnimationProperties? = null,
            animated: Boolean = false,
            endListener: DynamicAnimation.OnAnimationEndListener? = null,
            maxOvershoot: Float? = null,
        ) {
            if (animated) {
                startAnimation(view, animatableProperty, newEndValue, properties, endListener)
                startAnimation(
                    view,
                    animatableProperty,
                    newEndValue,
                    properties,
                    endListener,
                    maxOvershoot,
                )
            } else {
                animatableProperty.setFinalValue(view, newEndValue)
            }
@@ -131,6 +152,8 @@ class PhysicsPropertyAnimator {
            val (_, _, animator, _) = obtainPropertyData(view, property)
            return animator?.isRunning ?: false
        }

        internal val TAG = "PhysicsPropertyAnimator"
    }
}

@@ -140,6 +163,7 @@ private fun startAnimation(
    newEndValue: Float,
    properties: AnimationProperties?,
    endListener: DynamicAnimation.OnAnimationEndListener?,
    maxOvershoot: Float?,
) {
    val property = animatableProperty.property
    val propertyData = obtainPropertyData(view, animatableProperty)
@@ -168,8 +192,10 @@ private fun startAnimation(
            propertyData.offset = 0f
        }
    }
    if (animatableProperty.avoidDoubleOvershoot
        && propertyData.doubleOvershootAvoidingListener == null) {
    if (
        animatableProperty.avoidDoubleOvershoot &&
            propertyData.doubleOvershootAvoidingListener == null
    ) {
        propertyData.doubleOvershootAvoidingListener =
            DynamicAnimation.OnAnimationUpdateListener { _, offset: Float, velocity: Float ->
                val isOscillatingBackwards = velocity.sign == propertyData.startOffset.sign
@@ -179,25 +205,39 @@ private fun startAnimation(
                if (isOvershooting && isOscillatingBackwards && !didAlreadyRemoveBounciness) {
                    // our offset is starting to decrease, let's remove all overshoot
                    animator.spring.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
                } else if (!isOvershooting
                    && (didAlreadyRemoveBounciness || isOscillatingBackwards)) {
                } else if (
                    !isOvershooting && (didAlreadyRemoveBounciness || isOscillatingBackwards)
                ) {
                    // we already did overshoot, let's skip to the end to avoid oscillations.
                    // Usually we shouldn't hit this as setting the damping ratio avoid overshoots
                    // but it may still happen if we see jank
                    animator.skipToEnd();
                    animator.skipToEnd()
                }
            }
        animator.addUpdateListener(propertyData.doubleOvershootAvoidingListener)
    } else if (!animatableProperty.avoidDoubleOvershoot
        && propertyData.doubleOvershootAvoidingListener != null) {
    } else if (
        !animatableProperty.avoidDoubleOvershoot &&
            propertyData.doubleOvershootAvoidingListener != null
    ) {
        animator.removeUpdateListener(propertyData.doubleOvershootAvoidingListener)
    }

    val startOffset = previousEndValue - newEndValue + propertyData.offset

    // reset a new spring as it may have been modified
    animator.setSpring(createDefaultSpring().setFinalPosition(0f))
    val spring = createDefaultSpring().setFinalPosition(0f)
    maxOvershoot
        ?.takeIf { it > 0f }
        ?.let {
            // The spring will animate from [startOffset] to 0. Modify the spring parameters to
            // guarantee the overshoot won't exceed [maxOvershoot].
            spring.limitOvershoot(initialDisplacement = startOffset, maxOvershoot = it)
        }
    animator.setSpring(spring)

    // TODO(b/393581344): look at custom spring
    endListener?.let { animator.addEndListener(it) }

    val startOffset = previousEndValue - newEndValue + propertyData.offset
    // Immediately set the new offset that compensates for the immediate end value change
    propertyData.offset = startOffset
    propertyData.startOffset = startOffset
@@ -213,26 +253,29 @@ private fun startAnimation(
        // conditions and will never actually end them only calling start explicitly does that,
        // so let's start them again!
        animator.start()
        propertyData.endedBeforeStartingCleanupHandler = null;
        propertyData.endedBeforeStartingCleanupHandler = null
    }
    propertyData.endedBeforeStartingCleanupHandler = { cancelled ->
        val listener = properties?.getAnimationEndListener(animatableProperty.property)
        listener?.onAnimationEnd(propertyData.animator,
        listener?.onAnimationEnd(
            propertyData.animator,
            cancelled,
            0f /* value */,
            0f /* velocity */
            0f, /* velocity */
        )
        endListener?.onAnimationEnd(propertyData.animator,
        endListener?.onAnimationEnd(
            propertyData.animator,
            cancelled,
            0f /* value */,
            0f /* velocity */)
            0f, /* velocity */
        )
        propertyData.animator = null
        propertyData.doubleOvershootAvoidingListener = null
        propertyData.offset = 0f
        // We always reset the offset as we never want to get stuck with old values. This is
        // consistent with the end listener above.
        property.set(view, propertyData.finalValue)
        propertyData.endedBeforeStartingCleanupHandler = null;
        propertyData.endedBeforeStartingCleanupHandler = null
    }
    if (properties != null && properties.delay > 0 && !animator.isRunning) {
        propertyData.delayRunnable = startRunnable
@@ -251,3 +294,74 @@ private fun obtainPropertyData(view: View, animatableProperty: PhysicsProperty):
    }
    return propertyData
}

/**
 * Modifies this spring's parameters to guarantee it overshoots by at most [maxOvershoot], when
 * started with [initialDisplacement] and an initial velocity of 0.
 *
 * This requires the current spring parameters to be under-damped.
 */
@VisibleForTesting
fun SpringForce.limitOvershoot(initialDisplacement: Float, maxOvershoot: Float) {
    require(maxOvershoot > 0)
    val absoluteDisplacement = initialDisplacement.absoluteValue.toDouble()
    if (absoluteDisplacement == 0.0) {
        // Nothing to animate, cannot compute the constraint
        return
    }

    val originalStiffness = stiffness.toDouble()
    val originalDamping = dampingRatio.toDouble()
    if (originalDamping <= 0 || originalDamping >= 1) {
        Log.w(
            TAG,
            "limitOvershoot can be applied to under-damped springs only, but is $originalDamping",
        )
        return
    }

    if (maxOvershoot >= absoluteDisplacement) {
        // the overshoot is guaranteed to be less than absoluteDisplacement, so we don't need to
        // adjust
        return
    }

    // Calculate required damping to guarantee the overshoot won't exceed maxOvershoot.
    val lnOvershootRatio = ln(absoluteDisplacement / maxOvershoot)
    val requiredDamping = lnOvershootRatio / sqrt(PI.pow(2) + lnOvershootRatio.pow(2))
    if (requiredDamping < originalDamping) {
        // The current damping is already sufficient to not exceed the maxOvershoot. No need to
        // modify the spring
        return
    }

    if (requiredDamping >= 1) {
        // A critically / over-damped spring would never overshoot. Given the initial conditions
        // above, the branch should not be reached. Log a warning and don't modify the spring, this
        // is a state we did not expect.
        Log.w(
            TAG,
            "Unexpected required damping of $requiredDamping. " +
                "(original: $originalDamping, " +
                "displacement: $absoluteDisplacement, " +
                "maxOvershoot: $maxOvershoot)",
        )
        return
    }

    // The requiredDamping computed above guarantees that the overshoot won't exceed maxOvershoot
    // Now tweak the stiffness to compensate for the shift in frequency. We do this by aligning the
    // first 0-crossing with the original parameters.

    // Compute the time 0 is crossed, assuming the spring starts with 0 initial velocity.
    val omegaN = sqrt(originalStiffness) // Natural frequency
    val omegaD = omegaN * sqrt(1f - originalDamping.pow(2)) // Damped frequency
    val targetTime = (PI / 2f + asin(originalDamping)) / omegaD

    val numerator = PI / 2.0 + asin(requiredDamping)
    val denominator = targetTime * sqrt(1.0 - requiredDamping.pow(2))
    val requiredStiffness = (numerator / denominator).pow(2)

    setStiffness(requiredStiffness.toFloat())
    setDampingRatio(requiredDamping.toFloat())
}
+15 −2
Original line number Diff line number Diff line
@@ -192,6 +192,7 @@ public class ExpandableViewState extends ViewState {
        if (this.height != expandableView.getActualHeight()) {
            if (mUsePhysicsForMovement) {
                boolean animateHeight = properties.getAnimationFilter().animateHeight;
                float maxOvershoot = Float.POSITIVE_INFINITY;
                if (animateHeight) {
                    expandableView.setActualHeightAnimating(true);
                }
@@ -204,10 +205,22 @@ public class ExpandableViewState extends ViewState {
                            row.setGroupExpansionChanging(false /* isExpansionChanging */);
                        }
                    };

                    float targetHeight = this.height;
                    float currentHeight = expandableView.getActualHeight();
                    if (targetHeight < currentHeight && targetHeight > 0) {
                        // Avoid elements become invisible / very squished when collapsing a large
                        // list, for example bundle headers. In cases where the start height is
                        // large, the resulting overshoot could render the collapsed element
                        // temporarily invisible.

                        // This heuristic to limits the overshoot when collapsing an element to
                        // never squish it by more than a quarter of its target size.
                        maxOvershoot = targetHeight / 4f;
                    }
                }
                PhysicsPropertyAnimator.setProperty(child, HEIGHT_PROPERTY, this.height, properties,
                        animateHeight,
                        endListener);
                        animateHeight, endListener, maxOvershoot);
            } else {
                startHeightAnimationInterpolator(expandableView, properties);
            }
+20 −14
Original line number Diff line number Diff line
@@ -1444,22 +1444,28 @@ class ActivityClientController extends IActivityClientController.Stub {
            if (DesktopExperienceFlags.ENABLE_DESKTOP_WINDOWING_ENTERPRISE_BUGFIX.isTrue()
                    && mService.getTransitionController().isShellTransitionsEnabled()) {
                final Task task = r.getTask();
                final Transition transition = new Transition(TRANSIT_START_LOCK_TASK_MODE,

                if (!mService.canEnterLockTaskMode(task) || !mService.isTopMostTask(task)) {
                    Slog.w(TAG, "startLockTaskMode: Can't lock due to auth, task: " + task);
                    throw new IllegalArgumentException(
                            "startLockTaskMode: Can't lock due to auth, task: " + task);
                }
                ActionChain chain = mService.mChainTracker.startTransit("startLockTaskModeByToken");
                Transition newTransition = chain.isCollecting() ? null : new Transition(
                        TRANSIT_START_LOCK_TASK_MODE,
                        0 /* flags */,
                        mService.getTransitionController(), mService.mWindowManager.mSyncEngine);
                mService.getTransitionController().startCollectOrQueue(transition,
                        (deferred) -> {
                            final ActionChain chain = mService.mChainTracker.start(
                                    "startLockTaskModeByToken",
                                    transition);
                            mService.getTransitionController().requestStartTransition(transition,
                if (newTransition != null) {
                    mService.getTransitionController().requestStartTransition(newTransition,
                            task,
                            null /* remoteTransition */, null /* displayChange */);
                }
                chain.collect(task);
                mService.startLockTaskMode(task, false /* isSystemCaller */);
                            transition.setReady(task, true);
                if (newTransition != null) {
                    newTransition.setReady(task, true);
                }
                mService.mChainTracker.end();
                        });
            } else {
                mService.startLockTaskMode(r.getTask(), false /* isSystemCaller */);
            }
+20 −0
Original line number Diff line number Diff line
@@ -2696,11 +2696,20 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
                }
                if (DesktopExperienceFlags.ENABLE_DESKTOP_WINDOWING_ENTERPRISE_BUGFIX.isTrue()
                        && getTransitionController().isShellTransitionsEnabled()) {
                    if (!canEnterLockTaskMode(task)) {
                        Slog.w(TAG, "startLockTaskMode: Can't lock due to auth");
                        return;
                    }
                    final Transition transition = new Transition(TRANSIT_START_LOCK_TASK_MODE,
                            0 /* flags */,
                            getTransitionController(), mWindowManager.mSyncEngine);
                    getTransitionController().startCollectOrQueue(transition,
                            (deferred) -> {
                                if (deferred && !task.isAttached()) {
                                    Slog.w(TAG, "startLockTaskMode aborted: the task is removed.");
                                    transition.abort();
                                    return;
                                }
                                final ActionChain chain = mChainTracker.start(
                                        "startSystemLockTaskMOde",
                                        transition);
@@ -2789,6 +2798,17 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
        }
    }

    /** @return either a task can enter LockTask mode or not. */
    public boolean canEnterLockTaskMode(Task task) {
        return task.isAttached() && task.mLockTaskAuth != LOCK_TASK_AUTH_DONT_LOCK;
    }

    /** @return either a task is the top most or not. */
    public boolean isTopMostTask(Task task) {
        final Task rootTask = mRootWindowContainer.getTopDisplayFocusedRootTask();
        return rootTask != null && task == rootTask.getTopMostTask();
    }

    @Override
    public void updateLockTaskPackages(int userId, String[] packages) {
        final int callingUid = Binder.getCallingUid();