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

Commit a39e6a27 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Prevent SysUI from crashing during Ghost animation

This CL prevents a crash in SysUI when starting a launch animation that
ghosts a View that is not attached to a ViewGroup. We now skip the
animation when that happens.

Bug: 192194319
Test: atest ActivityLaunchAnimatorTest
Test: atest GhostedViewLaunchAnimatorControllerTest
Change-Id: I6f2616776a092307c84d8f64fbbb798eb1bebafb
parent 2b14f17e
Loading
Loading
Loading
Loading
+15 −5
Original line number Diff line number Diff line
@@ -35,6 +35,8 @@ import com.android.wm.shell.startingsurface.SplashscreenContentDrawer
import com.android.wm.shell.startingsurface.SplashscreenContentDrawer.SplashScreenWindowAttrs
import kotlin.math.roundToInt

private const val TAG = "ActivityLaunchAnimator"

/**
 * A class that allows activities to be started in a seamless way from a view that is transforming
 * nicely into the starting window.
@@ -43,8 +45,6 @@ class ActivityLaunchAnimator(
    private val keyguardHandler: KeyguardHandler,
    context: Context
) {
    private val TAG = this::class.java.simpleName

    companion object {
        const val ANIMATION_DURATION = 500L
        private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
@@ -233,11 +233,21 @@ class ActivityLaunchAnimator(
            /**
             * Return a [Controller] that will animate and expand [view] into the opening window.
             *
             * Important: The view must be attached to the window when calling this function and
             * during the animation.
             * Important: The view must be attached to a [ViewGroup] when calling this function and
             * during the animation. For safety, this method will return null when it is not.
             */
            @JvmStatic
            fun fromView(view: View, cujType: Int? = null): Controller {
            fun fromView(view: View, cujType: Int? = null): Controller? {
                if (view.parent !is ViewGroup) {
                    // TODO(b/192194319): Throw instead of just logging.
                    Log.wtf(
                        TAG,
                        "Skipping animation as view $view is not attached to a ViewGroup",
                        Exception()
                    )
                    return null
                }

                return GhostedViewLaunchAnimatorController(view, cujType)
            }
        }
+18 −3
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.LayerDrawable
import android.util.Log
import android.view.GhostView
import android.view.View
import android.view.ViewGroup
@@ -17,13 +18,15 @@ import android.widget.FrameLayout
import com.android.internal.jank.InteractionJankMonitor
import kotlin.math.min

private const val TAG = "GhostedViewLaunchAnimatorController"

/**
 * A base implementation of [ActivityLaunchAnimator.Controller] which creates a [ghost][GhostView]
 * of [ghostedView] as well as an expandable background view, which are drawn and animated instead
 * of the ghosted view.
 *
 * Important: [ghostedView] must be attached to the window when calling this function and during the
 * animation.
 * Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during
 * the animation.
 *
 * Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
 * whenever possible instead.
@@ -113,6 +116,13 @@ open class GhostedViewLaunchAnimatorController(
    }

    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
        if (ghostedView.parent !is ViewGroup) {
            // This should usually not happen, but let's make sure we don't crash if the view was
            // detached right before we started the animation.
            Log.w(TAG, "Skipping animation as ghostedView is not attached to a ViewGroup")
            return
        }

        backgroundView = FrameLayout(launchContainer.context)
        launchContainerOverlay.add(backgroundView)

@@ -138,7 +148,7 @@ open class GhostedViewLaunchAnimatorController(
        progress: Float,
        linearProgress: Float
    ) {
        val ghostView = this.ghostView!!
        val ghostView = this.ghostView ?: return
        val backgroundView = this.backgroundView!!

        if (!state.visible) {
@@ -173,6 +183,11 @@ open class GhostedViewLaunchAnimatorController(
    }

    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
        if (ghostView == null) {
            // We didn't actually run the animation.
            return
        }

        cujType?.let { InteractionJankMonitor.getInstance().end(it) }

        backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
+7 −0
Original line number Diff line number Diff line
@@ -475,6 +475,13 @@ public class MediaControlPanel {
    @Nullable
    private ActivityLaunchAnimator.Controller buildLaunchAnimatorController(
            TransitionLayout player) {
        if (!(player.getParent() instanceof ViewGroup)) {
            // TODO(b/192194319): Throw instead of just logging.
            Log.wtf(TAG, "Skipping player animation as it is not attached to a ViewGroup",
                    new Exception());
            return null;
        }

        // TODO(b/174236650): Make sure that the carousel indicator also fades out.
        // TODO(b/174236650): Instrument the animation to measure jank.
        return new GhostedViewLaunchAnimatorController(player,
+2 −1
Original line number Diff line number Diff line
@@ -514,7 +514,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
                                    InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
                            );
                    ActivityLaunchAnimator.Controller animationController =
                            new StatusBarLaunchAnimatorController(viewController, mStatusBar,
                            viewController == null ? null
                                : new StatusBarLaunchAnimatorController(viewController, mStatusBar,
                                    true /* isActivityIntent */);

                    mActivityLaunchAnimator.startIntentWithAnimation(animationController, animate,
+6 −0
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import android.view.IRemoteAnimationFinishedCallback
import android.view.RemoteAnimationAdapter
import android.view.RemoteAnimationTarget
import android.view.SurfaceControl
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
@@ -175,6 +176,11 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
        verify(controller).onLaunchAnimationStart(anyBoolean())
    }

    @Test
    fun controllerFromOrphanViewReturnsNull() {
        assertNull(ActivityLaunchAnimator.Controller.fromView(View(mContext)))
    }

    private fun fakeWindow(): RemoteAnimationTarget {
        val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
        val taskInfo = ActivityManager.RunningTaskInfo()
Loading