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

Commit 527fe9da authored by Johannes Gallmann's avatar Johannes Gallmann Committed by Android (Google) Code Review
Browse files

Merge "Introduce FlingOnBackAnimationCallback to make use of new timestamp API" into main

parents d565ccd3 e0b70381
Loading
Loading
Loading
Loading
+166 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.animation.back

import android.util.TimeUtils
import android.view.Choreographer
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_MOVE
import android.view.VelocityTracker
import android.view.animation.Interpolator
import android.window.BackEvent
import android.window.OnBackAnimationCallback
import com.android.app.animation.Interpolators
import com.android.internal.dynamicanimation.animation.DynamicAnimation
import com.android.internal.dynamicanimation.animation.FlingAnimation
import com.android.internal.dynamicanimation.animation.FloatValueHolder
import com.android.window.flags.Flags.predictiveBackTimestampApi

private const val FLING_FRICTION = 6f
private const val SCALE_FACTOR = 100f

/**
 * Enhanced [OnBackAnimationCallback] with automatic fling animation and interpolated progress.
 *
 * Simplifies back gesture handling by animating flings and emitting processed events through
 * `compat` functions. Customize progress interpolation with an optional [Interpolator].
 *
 * @param progressInterpolator [Interpolator] for progress, defaults to
 *   [Interpolators.BACK_GESTURE].
 */
abstract class FlingOnBackAnimationCallback(
    val progressInterpolator: Interpolator = Interpolators.BACK_GESTURE
) : OnBackAnimationCallback {

    private val velocityTracker = VelocityTracker.obtain()
    private var lastBackEvent: BackEvent? = null
    private var downTime: Long? = null

    private var backInvokedFlingAnim: FlingAnimation? = null
    private val backInvokedFlingUpdateListener =
        DynamicAnimation.OnAnimationUpdateListener { _, progress: Float, _ ->
            lastBackEvent?.let {
                val backEvent =
                    BackEvent(
                        it.touchX,
                        it.touchY,
                        progress / SCALE_FACTOR,
                        it.swipeEdge,
                        it.frameTime,
                    )
                onBackProgressedCompat(backEvent)
            }
        }
    private val backInvokedFlingEndListener =
        DynamicAnimation.OnAnimationEndListener { _, _, _, _ ->
            onBackInvokedCompat()
            reset()
        }

    abstract fun onBackStartedCompat(backEvent: BackEvent)

    abstract fun onBackProgressedCompat(backEvent: BackEvent)

    abstract fun onBackInvokedCompat()

    abstract fun onBackCancelledCompat()

    final override fun onBackStarted(backEvent: BackEvent) {
        if (backInvokedFlingAnim != null) {
            // This should never happen but let's call onBackInvokedCompat() just in case there is
            // still a fling animation in progress
            onBackInvokedCompat()
        }
        reset()
        if (predictiveBackTimestampApi()) {
            downTime = backEvent.frameTime
        }
        onBackStartedCompat(backEvent)
    }

    final override fun onBackProgressed(backEvent: BackEvent) {
        val interpolatedProgress = progressInterpolator.getInterpolation(backEvent.progress)
        if (predictiveBackTimestampApi()) {
            velocityTracker.addMovement(
                MotionEvent.obtain(
                    /* downTime */ downTime!!,
                    /* eventTime */ backEvent.frameTime,
                    /* action */ ACTION_MOVE,
                    /* x */ interpolatedProgress * SCALE_FACTOR,
                    /* y */ 0f,
                    /* metaState */ 0,
                )
            )
            lastBackEvent =
                BackEvent(
                    backEvent.touchX,
                    backEvent.touchY,
                    interpolatedProgress,
                    backEvent.swipeEdge,
                    backEvent.frameTime,
                )
        } else {
            lastBackEvent =
                BackEvent(
                    backEvent.touchX,
                    backEvent.touchY,
                    interpolatedProgress,
                    backEvent.swipeEdge,
                )
        }
        lastBackEvent?.let { onBackProgressedCompat(it) }
    }

    final override fun onBackInvoked() {
        if (predictiveBackTimestampApi() && lastBackEvent != null) {
            velocityTracker.computeCurrentVelocity(1000)
            backInvokedFlingAnim =
                FlingAnimation(FloatValueHolder())
                    .setStartValue((lastBackEvent?.progress ?: 0f) * SCALE_FACTOR)
                    .setFriction(FLING_FRICTION)
                    .setStartVelocity(velocityTracker.xVelocity)
                    .setMinValue(0f)
                    .setMaxValue(SCALE_FACTOR)
                    .also {
                        it.addUpdateListener(backInvokedFlingUpdateListener)
                        it.addEndListener(backInvokedFlingEndListener)
                        it.start()
                        // do an animation-frame immediately to prevent idle frame
                        it.doAnimationFrame(
                            Choreographer.getInstance().lastFrameTimeNanos / TimeUtils.NANOS_PER_MS
                        )
                    }
        } else {
            onBackInvokedCompat()
            reset()
        }
    }

    final override fun onBackCancelled() {
        onBackCancelledCompat()
        reset()
    }

    private fun reset() {
        velocityTracker.clear()
        backInvokedFlingAnim?.removeEndListener(backInvokedFlingEndListener)
        backInvokedFlingAnim?.removeUpdateListener(backInvokedFlingUpdateListener)
        lastBackEvent = null
        backInvokedFlingAnim = null
        downTime = null
    }
}
+7 −6
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.window.BackEvent
import android.window.OnBackAnimationCallback
import android.window.OnBackInvokedDispatcher
import android.window.OnBackInvokedDispatcher.Priority
import com.android.app.animation.Interpolators

/**
 * Generates an [OnBackAnimationCallback] given a [backAnimationSpec]. [onBackProgressed] will be
@@ -40,16 +41,16 @@ fun onBackAnimationCallbackFrom(
    onBackInvoked: () -> Unit = {},
    onBackCancelled: () -> Unit = {},
): OnBackAnimationCallback {
    return object : OnBackAnimationCallback {
    return object : FlingOnBackAnimationCallback(progressInterpolator = Interpolators.LINEAR) {
        private var initialY = 0f
        private val lastTransformation = BackTransformation()

        override fun onBackStarted(backEvent: BackEvent) {
        override fun onBackStartedCompat(backEvent: BackEvent) {
            initialY = backEvent.touchY
            onBackStarted(backEvent)
        }

        override fun onBackProgressed(backEvent: BackEvent) {
        override fun onBackProgressedCompat(backEvent: BackEvent) {
            val progressY = (backEvent.touchY - initialY) / displayMetrics.heightPixels

            backAnimationSpec.getBackTransformation(
@@ -61,11 +62,11 @@ fun onBackAnimationCallbackFrom(
            onBackProgressed(lastTransformation)
        }

        override fun onBackInvoked() {
        override fun onBackInvokedCompat() {
            onBackInvoked()
        }

        override fun onBackCancelled() {
        override fun onBackCancelledCompat() {
            onBackCancelled()
        }
    }
@@ -86,7 +87,7 @@ fun View.registerOnBackInvokedCallbackOnViewAttached(
            override fun onViewAttachedToWindow(v: View) {
                onBackInvokedDispatcher.registerOnBackInvokedCallback(
                    priority,
                    onBackAnimationCallback
                    onBackAnimationCallback,
                )
            }

+127 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.systemui.animation.back

import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.view.animation.Interpolator
import android.window.BackEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.app.animation.Interpolators
import com.android.systemui.SysuiTestCase
import com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_TIMESTAMP_API
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito

@SmallTest
@RunWith(AndroidJUnit4::class)
class FlingOnBackAnimationCallbackTest : SysuiTestCase() {

    @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()

    @Test
    fun testProgressInterpolation() {
        val mockInterpolator = Mockito.mock(Interpolator::class.java)
        val backEvent = backEventOf(0.5f)
        Mockito.`when`(mockInterpolator.getInterpolation(0.5f)).thenReturn(0.8f)
        val callback = TestFlingOnBackAnimationCallback(mockInterpolator)
        callback.onBackStarted(backEvent)
        assertTrue("Assert onBackStartedCompat called", callback.backStartedCalled)
        callback.onBackProgressed(backEvent)
        assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled)
        assertEquals("Assert interpolated progress", 0.8f, callback.progressEvent?.progress)
    }

    @Test
    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_TIMESTAMP_API)
    fun testFling() {
        val callback = TestFlingOnBackAnimationCallback(Interpolators.LINEAR)
        callback.onBackStarted(backEventOf(progress = 0f, frameTime = 0))
        assertTrue("Assert onBackStartedCompat called", callback.backStartedCalled)
        callback.onBackProgressed(backEventOf(0f, 8))
        callback.onBackProgressed(backEventOf(0.2f, 16))
        callback.onBackProgressed(backEventOf(0.4f, 24))
        callback.onBackProgressed(backEventOf(0.6f, 32))
        assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled)
        assertEquals("Assert interpolated progress", 0.6f, callback.progressEvent?.progress)
        getInstrumentation().runOnMainSync { callback.onBackInvoked() }
        // Assert that onBackInvoked is not called immediately...
        assertFalse(callback.backInvokedCalled)
        // Instead the fling animation is played and eventually onBackInvoked is called.
        callback.backInvokedLatch.await(1000, TimeUnit.MILLISECONDS)
        assertTrue(callback.backInvokedCalled)
    }

    @Test
    @RequiresFlagsDisabled(FLAG_PREDICTIVE_BACK_TIMESTAMP_API)
    fun testCallbackWithoutTimestampApi() {
        // Assert that all callback methods are immediately forwarded
        val callback = TestFlingOnBackAnimationCallback(Interpolators.LINEAR)
        callback.onBackStarted(backEventOf(progress = 0f, frameTime = 0))
        assertTrue("Assert onBackStartedCompat called", callback.backStartedCalled)
        callback.onBackProgressed(backEventOf(0f, 8))
        assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled)
        callback.onBackInvoked()
        assertTrue("Assert onBackInvoked called", callback.backInvokedCalled)
        callback.onBackCancelled()
        assertTrue("Assert onBackCancelled called", callback.backCancelledCalled)
    }

    private fun backEventOf(progress: Float, frameTime: Long = 0): BackEvent {
        return BackEvent(10f, 10f, progress, 0, frameTime)
    }

    /** Helper class to expose the compat functions for testing */
    private class TestFlingOnBackAnimationCallback(progressInterpolator: Interpolator) :
        FlingOnBackAnimationCallback(progressInterpolator) {
        var backStartedCalled = false
        var backProgressedCalled = false
        var backInvokedCalled = false
        val backInvokedLatch = CountDownLatch(1)
        var backCancelledCalled = false
        var progressEvent: BackEvent? = null

        override fun onBackStartedCompat(backEvent: BackEvent) {
            backStartedCalled = true
        }

        override fun onBackProgressedCompat(backEvent: BackEvent) {
            backProgressedCalled = true
            progressEvent = backEvent
        }

        override fun onBackInvokedCompat() {
            backInvokedCalled = true
            backInvokedLatch.countDown()
        }

        override fun onBackCancelledCompat() {
            backCancelledCalled = true
        }
    }
}
+4 −2
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@@ -56,6 +57,7 @@ import android.window.WindowOnBackInvokedDispatcher;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
@@ -675,8 +677,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
        backCallback.onBackProgressed(event);
        verify(mBouncerViewDelegateBackCallback).onBackProgressed(eq(event));

        backCallback.onBackInvoked();
        verify(mBouncerViewDelegateBackCallback).onBackInvoked();
        InstrumentationRegistry.getInstrumentation().runOnMainSync(backCallback::onBackInvoked);
        verify(mBouncerViewDelegateBackCallback, timeout(1000)).onBackInvoked();

        backCallback.onBackCancelled();
        verify(mBouncerViewDelegateBackCallback).onBackCancelled();
+1 −3
Original line number Diff line number Diff line
@@ -32,7 +32,6 @@ import static androidx.constraintlayout.widget.ConstraintSet.START;
import static androidx.constraintlayout.widget.ConstraintSet.TOP;
import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;

import static com.android.app.animation.InterpolatorsAndroidX.DECELERATE_QUINT;
import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY;

import static java.lang.Integer.max;
@@ -271,8 +270,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout {
        public void onBackProgressed(BackEvent event) {
            float progress = event.getProgress();
            // TODO(b/263819310): Update the interpolator to match spec.
            float scale = MIN_BACK_SCALE
                    +  (1 - MIN_BACK_SCALE) * (1 - DECELERATE_QUINT.getInterpolation(progress));
            float scale = MIN_BACK_SCALE +  (1 - MIN_BACK_SCALE) * (1 - progress);
            setScale(scale);
        }
    };
Loading