Merge "Prevent SysUI from crashing during Ghost animation" into sc-dev am: e388c26283

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15235792

Change-Id: Ic42458bff3e10f88a2a1aa3d017363cfa8d3b82e
This commit is contained in:
Jordan Demeulenaere
2021-07-14 07:29:50 +00:00
committed by Automerger Merge Worker
6 changed files with 90 additions and 9 deletions

View File

@@ -34,6 +34,8 @@ import com.android.wm.shell.startingsurface.SplashscreenContentDrawer
import com.android.wm.shell.startingsurface.StartingSurface
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 startingSurface: StartingSurface?,
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)
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
@@ -177,6 +178,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()

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2021 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
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
@Test
fun animatingOrphanViewDoesNotCrash() {
val ghostedView = LinearLayout(mContext)
val controller = GhostedViewLaunchAnimatorController(ghostedView)
val state = ActivityLaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
controller.onIntentStarted(willAnimate = true)
controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
controller.onLaunchAnimationProgress(state, progress = 0f, linearProgress = 0f)
controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
}
}