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: I0c60205096c646fe6c3c0423e42f0fca2902fd02
This commit is contained in:
committed by
Automerger Merge Worker
commit
44b439bae8
@@ -34,6 +34,8 @@ import com.android.wm.shell.startingsurface.SplashscreenContentDrawer
|
|||||||
import com.android.wm.shell.startingsurface.StartingSurface
|
import com.android.wm.shell.startingsurface.StartingSurface
|
||||||
import kotlin.math.roundToInt
|
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
|
* A class that allows activities to be started in a seamless way from a view that is transforming
|
||||||
* nicely into the starting window.
|
* nicely into the starting window.
|
||||||
@@ -43,8 +45,6 @@ class ActivityLaunchAnimator(
|
|||||||
private val startingSurface: StartingSurface?,
|
private val startingSurface: StartingSurface?,
|
||||||
context: Context
|
context: Context
|
||||||
) {
|
) {
|
||||||
private val TAG = this::class.java.simpleName
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ANIMATION_DURATION = 500L
|
const val ANIMATION_DURATION = 500L
|
||||||
private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
|
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.
|
* 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
|
* Important: The view must be attached to a [ViewGroup] when calling this function and
|
||||||
* during the animation.
|
* during the animation. For safety, this method will return null when it is not.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@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)
|
return GhostedViewLaunchAnimatorController(view, cujType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.graphics.drawable.Drawable
|
|||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.graphics.drawable.InsetDrawable
|
import android.graphics.drawable.InsetDrawable
|
||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
|
import android.util.Log
|
||||||
import android.view.GhostView
|
import android.view.GhostView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -17,13 +18,15 @@ import android.widget.FrameLayout
|
|||||||
import com.android.internal.jank.InteractionJankMonitor
|
import com.android.internal.jank.InteractionJankMonitor
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
private const val TAG = "GhostedViewLaunchAnimatorController"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base implementation of [ActivityLaunchAnimator.Controller] which creates a [ghost][GhostView]
|
* 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 [ghostedView] as well as an expandable background view, which are drawn and animated instead
|
||||||
* of the ghosted view.
|
* of the ghosted view.
|
||||||
*
|
*
|
||||||
* Important: [ghostedView] must be attached to the window when calling this function and during the
|
* Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during
|
||||||
* animation.
|
* the animation.
|
||||||
*
|
*
|
||||||
* Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
|
* Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
|
||||||
* whenever possible instead.
|
* whenever possible instead.
|
||||||
@@ -113,6 +116,13 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
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)
|
backgroundView = FrameLayout(launchContainer.context)
|
||||||
launchContainerOverlay.add(backgroundView)
|
launchContainerOverlay.add(backgroundView)
|
||||||
|
|
||||||
@@ -138,7 +148,7 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
progress: Float,
|
progress: Float,
|
||||||
linearProgress: Float
|
linearProgress: Float
|
||||||
) {
|
) {
|
||||||
val ghostView = this.ghostView!!
|
val ghostView = this.ghostView ?: return
|
||||||
val backgroundView = this.backgroundView!!
|
val backgroundView = this.backgroundView!!
|
||||||
|
|
||||||
if (!state.visible) {
|
if (!state.visible) {
|
||||||
@@ -173,6 +183,11 @@ open class GhostedViewLaunchAnimatorController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
|
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
|
||||||
|
if (ghostView == null) {
|
||||||
|
// We didn't actually run the animation.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cujType?.let { InteractionJankMonitor.getInstance().end(it) }
|
cujType?.let { InteractionJankMonitor.getInstance().end(it) }
|
||||||
|
|
||||||
backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
|
backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
|
||||||
|
|||||||
@@ -475,6 +475,13 @@ public class MediaControlPanel {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private ActivityLaunchAnimator.Controller buildLaunchAnimatorController(
|
private ActivityLaunchAnimator.Controller buildLaunchAnimatorController(
|
||||||
TransitionLayout player) {
|
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): Make sure that the carousel indicator also fades out.
|
||||||
// TODO(b/174236650): Instrument the animation to measure jank.
|
// TODO(b/174236650): Instrument the animation to measure jank.
|
||||||
return new GhostedViewLaunchAnimatorController(player,
|
return new GhostedViewLaunchAnimatorController(player,
|
||||||
|
|||||||
@@ -514,7 +514,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
|
|||||||
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
|
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
|
||||||
);
|
);
|
||||||
ActivityLaunchAnimator.Controller animationController =
|
ActivityLaunchAnimator.Controller animationController =
|
||||||
new StatusBarLaunchAnimatorController(viewController, mStatusBar,
|
viewController == null ? null
|
||||||
|
: new StatusBarLaunchAnimatorController(viewController, mStatusBar,
|
||||||
true /* isActivityIntent */);
|
true /* isActivityIntent */);
|
||||||
|
|
||||||
mActivityLaunchAnimator.startIntentWithAnimation(animationController, animate,
|
mActivityLaunchAnimator.startIntentWithAnimation(animationController, animate,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import android.view.IRemoteAnimationFinishedCallback
|
|||||||
import android.view.RemoteAnimationAdapter
|
import android.view.RemoteAnimationAdapter
|
||||||
import android.view.RemoteAnimationTarget
|
import android.view.RemoteAnimationTarget
|
||||||
import android.view.SurfaceControl
|
import android.view.SurfaceControl
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
@@ -177,6 +178,11 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
|||||||
verify(controller).onLaunchAnimationStart(anyBoolean())
|
verify(controller).onLaunchAnimationStart(anyBoolean())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun controllerFromOrphanViewReturnsNull() {
|
||||||
|
assertNull(ActivityLaunchAnimator.Controller.fromView(View(mContext)))
|
||||||
|
}
|
||||||
|
|
||||||
private fun fakeWindow(): RemoteAnimationTarget {
|
private fun fakeWindow(): RemoteAnimationTarget {
|
||||||
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
|
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
|
||||||
val taskInfo = ActivityManager.RunningTaskInfo()
|
val taskInfo = ActivityManager.RunningTaskInfo()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user