Animate dialog to app launches (1/2)
See b/214961273#comment1 and #comment2 for before/after videos as well as technical explanations on how this animation works. This CL also enables the generation of Java 8 default method bodies in interfaces, in the animation library. This is necessary to avoid unexpected behaviors when using @JvmDefault on an interface + delegation, which is a common pattern when using the animation library. See [1] and [2] for more info. Note that this is enabled only for the animation library (and not SystemUI(Lib)) to avoid breaking code depending on the (now deprecated) @JvmDefault method in SysUI. [1] https://youtrack.jetbrains.com/issue/KT-34612 [2] https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/ Bug: 214961273 Test: atest DialogLaunchAnimatorTest Test: Manual Change-Id: I27d53400a5dc852543a68f5b6f107d9eb08b06db
This commit is contained in:
@@ -39,5 +39,5 @@ android_library {
|
||||
],
|
||||
|
||||
manifest: "AndroidManifest.xml",
|
||||
kotlincflags: ["-Xjvm-default=enable"],
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
}
|
||||
|
||||
@@ -48,9 +48,16 @@ private const val TAG = "ActivityLaunchAnimator"
|
||||
* nicely into the starting window.
|
||||
*/
|
||||
class ActivityLaunchAnimator(
|
||||
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS)
|
||||
/** The animator used when animating a View into an app. */
|
||||
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
|
||||
|
||||
/** The animator used when animating a Dialog into an app. */
|
||||
// TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to
|
||||
// TIMINGS.contentBeforeFadeOutDuration.
|
||||
private val dialogToAppAnimator: LaunchAnimator = LaunchAnimator(DIALOG_TIMINGS, INTERPOLATORS)
|
||||
) {
|
||||
companion object {
|
||||
/** The timings when animating a View into an app. */
|
||||
@JvmField
|
||||
val TIMINGS = LaunchAnimator.Timings(
|
||||
totalDuration = 500L,
|
||||
@@ -60,6 +67,17 @@ class ActivityLaunchAnimator(
|
||||
contentAfterFadeInDuration = 183L
|
||||
)
|
||||
|
||||
/**
|
||||
* The timings when animating a Dialog into an app. We need to wait at least 200ms before
|
||||
* showing the app (which is under the dialog window) so that the dialog window dim is fully
|
||||
* faded out, to avoid flicker.
|
||||
*/
|
||||
val DIALOG_TIMINGS = TIMINGS.copy(
|
||||
contentBeforeFadeOutDuration = 200L,
|
||||
contentAfterFadeInDelay = 200L
|
||||
)
|
||||
|
||||
/** The interpolators when animating a View or a dialog into an app. */
|
||||
val INTERPOLATORS = LaunchAnimator.Interpolators(
|
||||
positionInterpolator = Interpolators.EMPHASIZED,
|
||||
positionXInterpolator = createPositionXInterpolator(),
|
||||
@@ -297,11 +315,18 @@ class ActivityLaunchAnimator(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this controller is controlling a dialog launch. This will be used to adapt the
|
||||
* timings, making sure we don't show the app until the dialog dim had the time to fade out.
|
||||
*/
|
||||
// TODO(b/218989950): Remove this.
|
||||
val isDialogLaunch: Boolean
|
||||
get() = false
|
||||
|
||||
/**
|
||||
* The intent was started. If [willAnimate] is false, nothing else will happen and the
|
||||
* animation will not be started.
|
||||
*/
|
||||
@JvmDefault
|
||||
fun onIntentStarted(willAnimate: Boolean) {}
|
||||
|
||||
/**
|
||||
@@ -309,7 +334,6 @@ class ActivityLaunchAnimator(
|
||||
* this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
|
||||
* before the cancellation.
|
||||
*/
|
||||
@JvmDefault
|
||||
fun onLaunchAnimationCancelled() {}
|
||||
}
|
||||
|
||||
@@ -317,7 +341,9 @@ class ActivityLaunchAnimator(
|
||||
inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
|
||||
private val launchContainer = controller.launchContainer
|
||||
private val context = launchContainer.context
|
||||
private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer)
|
||||
private val transactionApplierView =
|
||||
controller.openingWindowSyncView ?: controller.launchContainer
|
||||
private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView)
|
||||
|
||||
private val matrix = Matrix()
|
||||
private val invertMatrix = Matrix()
|
||||
@@ -405,6 +431,13 @@ class ActivityLaunchAnimator(
|
||||
val callback = this@ActivityLaunchAnimator.callback!!
|
||||
val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
|
||||
|
||||
// Make sure we use the modified timings when animating a dialog into an app.
|
||||
val launchAnimator = if (controller.isDialogLaunch) {
|
||||
dialogToAppAnimator
|
||||
} else {
|
||||
launchAnimator
|
||||
}
|
||||
|
||||
// TODO(b/184121838): We should somehow get the top and bottom radius of the window
|
||||
// instead of recomputing isExpandingFullyAbove here.
|
||||
val isExpandingFullyAbove =
|
||||
@@ -440,19 +473,29 @@ class ActivityLaunchAnimator(
|
||||
progress: Float,
|
||||
linearProgress: Float
|
||||
) {
|
||||
applyStateToWindow(window, state)
|
||||
// Apply the state to the window only if it is visible, i.e. when the expanding
|
||||
// view is *not* visible.
|
||||
if (!state.visible) {
|
||||
applyStateToWindow(window, state)
|
||||
}
|
||||
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
|
||||
|
||||
listeners.forEach { it.onLaunchAnimationProgress(linearProgress) }
|
||||
delegate.onLaunchAnimationProgress(state, progress, linearProgress)
|
||||
}
|
||||
}
|
||||
|
||||
// We draw a hole when the additional layer is fading out to reveal the opening window.
|
||||
animation = launchAnimator.startAnimation(
|
||||
controller, endState, windowBackgroundColor, drawHole = true)
|
||||
}
|
||||
|
||||
private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) {
|
||||
if (transactionApplierView.viewRootImpl == null) {
|
||||
// If the view root we synchronize with was detached, don't apply any transaction
|
||||
// (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
|
||||
return
|
||||
}
|
||||
|
||||
val screenBounds = window.screenSpaceBounds
|
||||
val centerX = (screenBounds.left + screenBounds.right) / 2f
|
||||
val centerY = (screenBounds.top + screenBounds.bottom) / 2f
|
||||
@@ -510,6 +553,12 @@ class ActivityLaunchAnimator(
|
||||
state: LaunchAnimator.State,
|
||||
linearProgress: Float
|
||||
) {
|
||||
if (transactionApplierView.viewRootImpl == null) {
|
||||
// If the view root we synchronize with was detached, don't apply any transaction
|
||||
// (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
|
||||
return
|
||||
}
|
||||
|
||||
val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress,
|
||||
ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ package com.android.systemui.animation
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.ActivityManager
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
@@ -28,12 +27,12 @@ import android.service.dreams.IDreamManager
|
||||
import android.util.Log
|
||||
import android.util.MathUtils
|
||||
import android.view.GhostView
|
||||
import android.view.SurfaceControl
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewRootImpl
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowManager
|
||||
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
|
||||
import android.widget.FrameLayout
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -42,12 +41,17 @@ private const val TAG = "DialogLaunchAnimator"
|
||||
/**
|
||||
* A class that allows dialogs to be started in a seamless way from a view that is transforming
|
||||
* nicely into the starting dialog.
|
||||
*
|
||||
* This animator also allows to easily animate a dialog into an activity.
|
||||
*
|
||||
* @see showFromView
|
||||
* @see showFromDialog
|
||||
* @see createActivityLaunchController
|
||||
*/
|
||||
class DialogLaunchAnimator @JvmOverloads constructor(
|
||||
private val dreamManager: IDreamManager,
|
||||
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
|
||||
// TODO(b/217621394): Remove special handling for low-RAM devices after animation sync is fixed
|
||||
private var forceDisableSynchronization: Boolean = ActivityManager.isLowRamDeviceStatic()
|
||||
private val isForTesting: Boolean = false
|
||||
) {
|
||||
private companion object {
|
||||
private val TIMINGS = ActivityLaunchAnimator.TIMINGS
|
||||
@@ -113,7 +117,7 @@ class DialogLaunchAnimator @JvmOverloads constructor(
|
||||
dialog = dialog,
|
||||
animateBackgroundBoundsChange,
|
||||
animatedParent,
|
||||
forceDisableSynchronization
|
||||
isForTesting
|
||||
)
|
||||
|
||||
openedDialogs.add(animatedDialog)
|
||||
@@ -140,6 +144,100 @@ class DialogLaunchAnimator @JvmOverloads constructor(
|
||||
showFromView(dialog, view, animateBackgroundBoundsChange)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from the
|
||||
* dialog that contains [View]. Note that the dialog must have been show using [showFromView]
|
||||
* and be currently showing, otherwise this will return null.
|
||||
*
|
||||
* The returned controller will take care of dismissing the dialog at the right time after the
|
||||
* activity started, when the dialog to app animation is done (or when it is cancelled). If this
|
||||
* method returns null, then the dialog won't be dismissed.
|
||||
*
|
||||
* @param view any view inside the dialog to animate.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun createActivityLaunchController(
|
||||
view: View,
|
||||
cujType: Int? = null
|
||||
): ActivityLaunchAnimator.Controller? {
|
||||
val animatedDialog = openedDialogs
|
||||
.firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl }
|
||||
?: return null
|
||||
|
||||
// At this point, we know that the intent of the caller is to dismiss the dialog to show
|
||||
// an app, so we disable the exit animation into the touch surface because we will never
|
||||
// want to run it anyways.
|
||||
animatedDialog.exitAnimationDisabled = true
|
||||
|
||||
val dialog = animatedDialog.dialog
|
||||
|
||||
// Don't animate if the dialog is not showing.
|
||||
if (!dialog.isShowing) {
|
||||
return null
|
||||
}
|
||||
|
||||
val dialogContentWithBackground = animatedDialog.dialogContentWithBackground ?: return null
|
||||
val controller =
|
||||
ActivityLaunchAnimator.Controller.fromView(dialogContentWithBackground, cujType)
|
||||
?: return null
|
||||
|
||||
// Wrap the controller into one that will instantly dismiss the dialog when the animation is
|
||||
// done or dismiss it normally (fading it out) if the animation is cancelled.
|
||||
return object : ActivityLaunchAnimator.Controller by controller {
|
||||
override val isDialogLaunch = true
|
||||
|
||||
override fun onIntentStarted(willAnimate: Boolean) {
|
||||
controller.onIntentStarted(willAnimate)
|
||||
|
||||
if (!willAnimate) {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLaunchAnimationCancelled() {
|
||||
controller.onLaunchAnimationCancelled()
|
||||
enableDialogDismiss()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
|
||||
controller.onLaunchAnimationStart(isExpandingFullyAbove)
|
||||
|
||||
// Make sure the dialog is not dismissed during the animation.
|
||||
disableDialogDismiss()
|
||||
|
||||
// If this dialog was shown from a cascade of other dialogs, make sure those ones
|
||||
// are dismissed too.
|
||||
animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss()
|
||||
|
||||
// Remove the dim.
|
||||
dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
}
|
||||
|
||||
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
|
||||
controller.onLaunchAnimationEnd(isExpandingFullyAbove)
|
||||
|
||||
// Hide the dialog then dismiss it to instantly dismiss it without playing the
|
||||
// animation.
|
||||
dialog.hide()
|
||||
enableDialogDismiss()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
private fun disableDialogDismiss() {
|
||||
dialog.setDismissOverride { /* Do nothing */ }
|
||||
}
|
||||
|
||||
private fun enableDialogDismiss() {
|
||||
// We don't set the override to null given that [AnimatedDialog.OnDialogDismissed]
|
||||
// will still properly dismiss the dialog but will also make sure to clean up
|
||||
// everything (like making sure that the touched view that triggered the dialog is
|
||||
// made VISIBLE again).
|
||||
dialog.setDismissOverride(animatedDialog::onDialogDismissed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all dialogs currently shown won't animate into their touch surface when
|
||||
* dismissed.
|
||||
@@ -358,6 +456,21 @@ private class AnimatedDialog(
|
||||
// Make sure the dialog is visible instantly and does not do any window animation.
|
||||
window.attributes.windowAnimations = R.style.Animation_LaunchAnimation
|
||||
|
||||
// Ensure that the animation is not clipped by the display cut-out when animating this
|
||||
// dialog into an app.
|
||||
window.attributes.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
|
||||
window.attributes = window.attributes
|
||||
|
||||
// We apply the insets ourselves to make sure that the paddings are set on the correct
|
||||
// View.
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup)
|
||||
viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets ->
|
||||
val insets = windowInsets.getInsets(WindowInsets.Type.displayCutout())
|
||||
view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
|
||||
WindowInsets.CONSUMED
|
||||
}
|
||||
|
||||
// Start the animation once the background view is properly laid out.
|
||||
dialogContentWithBackground.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
|
||||
override fun onLayoutChange(
|
||||
@@ -421,45 +534,12 @@ private class AnimatedDialog(
|
||||
* (or inversely, removed from the UI when the touch surface is made visible).
|
||||
*/
|
||||
private fun synchronizeNextDraw(then: () -> Unit) {
|
||||
if (forceDisableSynchronization ||
|
||||
!touchSurface.isAttachedToWindow || touchSurface.viewRootImpl == null ||
|
||||
!decorView.isAttachedToWindow || decorView.viewRootImpl == null) {
|
||||
// No need to synchronize if either the touch surface or dialog view is not attached
|
||||
// to a window.
|
||||
if (forceDisableSynchronization) {
|
||||
then()
|
||||
return
|
||||
}
|
||||
|
||||
// Consume the next frames of both view roots to make sure the ghost view is drawn at
|
||||
// exactly the same time as when the touch surface is made invisible.
|
||||
var remainingTransactions = 0
|
||||
val mergedTransactions = SurfaceControl.Transaction()
|
||||
|
||||
fun onTransaction(transaction: SurfaceControl.Transaction?) {
|
||||
remainingTransactions--
|
||||
transaction?.let { mergedTransactions.merge(it) }
|
||||
|
||||
if (remainingTransactions == 0) {
|
||||
mergedTransactions.apply()
|
||||
then()
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
|
||||
if (viewRootImpl.consumeNextDraw(::onTransaction)) {
|
||||
remainingTransactions++
|
||||
|
||||
// Make sure we trigger a traversal.
|
||||
viewRootImpl.view.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
consumeNextDraw(touchSurface.viewRootImpl)
|
||||
consumeNextDraw(decorView.viewRootImpl)
|
||||
|
||||
if (remainingTransactions == 0) {
|
||||
then()
|
||||
}
|
||||
ViewRootSync.synchronizeNextDraw(touchSurface, decorView, then)
|
||||
}
|
||||
|
||||
private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
|
||||
@@ -523,7 +603,7 @@ private class AnimatedDialog(
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDialogDismissed() {
|
||||
fun onDialogDismissed() {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
dialog.context.mainExecutor.execute { onDialogDismissed() }
|
||||
return
|
||||
|
||||
@@ -77,8 +77,8 @@ class LaunchAnimator(
|
||||
* This will be used to:
|
||||
* - Get the associated [Context].
|
||||
* - Compute whether we are expanding fully above the launch container.
|
||||
* - Apply surface transactions in sync with RenderThread when animating an activity
|
||||
* launch.
|
||||
* - Get to overlay to which we initially put the window background layer, until the
|
||||
* opening window is made visible (see [openingWindowSyncView]).
|
||||
*
|
||||
* This container can be changed to force this [Controller] to animate the expanding view
|
||||
* inside a different location, for instance to ensure correct layering during the
|
||||
@@ -86,6 +86,18 @@ class LaunchAnimator(
|
||||
*/
|
||||
var launchContainer: ViewGroup
|
||||
|
||||
/**
|
||||
* The [View] with which the opening app window should be synchronized with once it starts
|
||||
* to be visible.
|
||||
*
|
||||
* We will also move the window background layer to this view's overlay once the opening
|
||||
* window is visible.
|
||||
*
|
||||
* If null, this will default to [launchContainer].
|
||||
*/
|
||||
val openingWindowSyncView: View?
|
||||
get() = null
|
||||
|
||||
/**
|
||||
* Return the [State] of the view that will be animated. We will animate from this state to
|
||||
* the final window state.
|
||||
@@ -100,11 +112,9 @@ class LaunchAnimator(
|
||||
* needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
|
||||
* fully above the [launchContainer].
|
||||
*/
|
||||
@JvmDefault
|
||||
fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
|
||||
|
||||
/** The animation made progress and the expandable view [state] should be updated. */
|
||||
@JvmDefault
|
||||
fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
|
||||
|
||||
/**
|
||||
@@ -112,7 +122,6 @@ class LaunchAnimator(
|
||||
* called previously. This is typically used to clean up the resources initialized when the
|
||||
* animation was started.
|
||||
*/
|
||||
@JvmDefault
|
||||
fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
|
||||
}
|
||||
|
||||
@@ -154,7 +163,7 @@ class LaunchAnimator(
|
||||
}
|
||||
|
||||
/** The timings (durations and delays) used by this animator. */
|
||||
class Timings(
|
||||
data class Timings(
|
||||
/** The total duration of the animation. */
|
||||
val totalDuration: Long,
|
||||
|
||||
@@ -257,8 +266,17 @@ class LaunchAnimator(
|
||||
animator.duration = timings.totalDuration
|
||||
animator.interpolator = LINEAR
|
||||
|
||||
// Whether we should move the [windowBackgroundLayer] into the overlay of
|
||||
// [Controller.openingWindowSyncView] once the opening app window starts to be visible.
|
||||
val openingWindowSyncView = controller.openingWindowSyncView
|
||||
val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
|
||||
val moveBackgroundLayerWhenAppIsVisible = openingWindowSyncView != null &&
|
||||
openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl
|
||||
|
||||
val launchContainerOverlay = launchContainer.overlay
|
||||
var cancelled = false
|
||||
var movedBackgroundLayer = false
|
||||
|
||||
animator.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
|
||||
if (DEBUG) {
|
||||
@@ -278,6 +296,10 @@ class LaunchAnimator(
|
||||
}
|
||||
controller.onLaunchAnimationEnd(isExpandingFullyAbove)
|
||||
launchContainerOverlay.remove(windowBackgroundLayer)
|
||||
|
||||
if (moveBackgroundLayerWhenAppIsVisible) {
|
||||
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -318,11 +340,29 @@ class LaunchAnimator(
|
||||
timings.contentBeforeFadeOutDuration
|
||||
) < 1
|
||||
|
||||
if (moveBackgroundLayerWhenAppIsVisible && !state.visible && !movedBackgroundLayer) {
|
||||
// The expanding view is not visible, so the opening app is visible. If this is the
|
||||
// first frame when it happens, trigger a one-off sync and move the background layer
|
||||
// in its new container.
|
||||
movedBackgroundLayer = true
|
||||
|
||||
launchContainerOverlay.remove(windowBackgroundLayer)
|
||||
openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)
|
||||
|
||||
ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {})
|
||||
}
|
||||
|
||||
val container = if (movedBackgroundLayer) {
|
||||
openingWindowSyncView!!
|
||||
} else {
|
||||
controller.launchContainer
|
||||
}
|
||||
|
||||
applyStateToWindowBackgroundLayer(
|
||||
windowBackgroundLayer,
|
||||
state,
|
||||
linearProgress,
|
||||
launchContainer,
|
||||
container,
|
||||
drawHole
|
||||
)
|
||||
controller.onLaunchAnimationProgress(state, progress, linearProgress)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.android.systemui.animation
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.view.SurfaceControl
|
||||
import android.view.View
|
||||
import android.view.ViewRootImpl
|
||||
|
||||
/** A util class to synchronize 2 view roots. */
|
||||
// TODO(b/200284684): Remove this class.
|
||||
object ViewRootSync {
|
||||
// TODO(b/217621394): Remove special handling for low-RAM devices after animation sync is fixed
|
||||
private val forceDisableSynchronization = ActivityManager.isLowRamDeviceStatic()
|
||||
|
||||
/**
|
||||
* Synchronize the next draw between the view roots of [view] and [otherView], then run [then].
|
||||
*
|
||||
* Note that in some cases, the synchronization might not be possible (e.g. WM consumed the
|
||||
* next transactions) or disabled (temporarily, on low ram devices). In this case, [then] will
|
||||
* be called without synchronizing.
|
||||
*/
|
||||
fun synchronizeNextDraw(
|
||||
view: View,
|
||||
otherView: View,
|
||||
then: () -> Unit
|
||||
) {
|
||||
if (forceDisableSynchronization ||
|
||||
!view.isAttachedToWindow || view.viewRootImpl == null ||
|
||||
!otherView.isAttachedToWindow || otherView.viewRootImpl == null ||
|
||||
view.viewRootImpl == otherView.viewRootImpl) {
|
||||
// No need to synchronize if either the touch surface or dialog view is not attached
|
||||
// to a window.
|
||||
then()
|
||||
return
|
||||
}
|
||||
|
||||
// Consume the next frames of both view roots to make sure the ghost view is drawn at
|
||||
// exactly the same time as when the touch surface is made invisible.
|
||||
var remainingTransactions = 0
|
||||
val mergedTransactions = SurfaceControl.Transaction()
|
||||
|
||||
fun onTransaction(transaction: SurfaceControl.Transaction?) {
|
||||
remainingTransactions--
|
||||
transaction?.let { mergedTransactions.merge(it) }
|
||||
|
||||
if (remainingTransactions == 0) {
|
||||
mergedTransactions.apply()
|
||||
then()
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
|
||||
if (viewRootImpl.consumeNextDraw(::onTransaction)) {
|
||||
remainingTransactions++
|
||||
|
||||
// Make sure we trigger a traversal.
|
||||
viewRootImpl.view.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
consumeNextDraw(view.viewRootImpl)
|
||||
consumeNextDraw(otherView.viewRootImpl)
|
||||
|
||||
if (remainingTransactions == 0) {
|
||||
then()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Java-friendly API for [synchronizeNextDraw].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun synchronizeNextDraw(view: View, otherView: View, then: Runnable) {
|
||||
synchronizeNextDraw(view, otherView, then::run)
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
|
||||
d.setColorFilter(new PorterDuffColorFilter(
|
||||
Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN));
|
||||
mTitleIcon.setImageDrawable(d);
|
||||
mContainerLayout.setOnClickListener(v -> onItemClick(CUSTOMIZED_ITEM_PAIR_NEW));
|
||||
mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,11 +241,5 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void onItemClick(int customizedItem) {
|
||||
if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
|
||||
mController.launchBluetoothPairing();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ import com.android.settingslib.media.LocalMediaManager;
|
||||
import com.android.settingslib.media.MediaDevice;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||
import com.android.systemui.monet.ColorScheme;
|
||||
import com.android.systemui.plugins.ActivityStarter;
|
||||
@@ -533,12 +534,14 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
||||
return false;
|
||||
}
|
||||
|
||||
void launchBluetoothPairing() {
|
||||
// Dismissing a dialog into its touch surface and starting an activity at the same time
|
||||
// looks bad, so let's make sure the dialog just fades out quickly.
|
||||
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
|
||||
void launchBluetoothPairing(View view) {
|
||||
ActivityLaunchAnimator.Controller controller =
|
||||
mDialogLaunchAnimator.createActivityLaunchController(view);
|
||||
|
||||
if (controller == null) {
|
||||
mCallback.dismissDialog();
|
||||
}
|
||||
|
||||
mCallback.dismissDialog();
|
||||
Intent launchIntent =
|
||||
new Intent(ACTION_BLUETOOTH_PAIRING_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
@@ -553,10 +556,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
|
||||
deepLinkIntent.putExtra(
|
||||
Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY,
|
||||
PAGE_CONNECTED_DEVICES_KEY);
|
||||
mActivityStarter.startActivity(deepLinkIntent, true);
|
||||
mActivityStarter.startActivity(deepLinkIntent, true, controller);
|
||||
return;
|
||||
}
|
||||
mActivityStarter.startActivity(launchIntent, true);
|
||||
mActivityStarter.startActivity(launchIntent, true, controller);
|
||||
}
|
||||
|
||||
void launchMediaOutputGroupDialog(View mediaOutputDialog) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.android.internal.app.MediaRouteDialogPresenter;
|
||||
import com.android.internal.logging.MetricsLogger;
|
||||
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||
import com.android.systemui.dagger.qualifiers.Background;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
@@ -191,10 +192,16 @@ public class CastTile extends QSTileImpl<BooleanState> {
|
||||
mContext,
|
||||
ROUTE_TYPE_REMOTE_DISPLAY,
|
||||
v -> {
|
||||
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
|
||||
holder.mDialog.dismiss();
|
||||
ActivityLaunchAnimator.Controller controller =
|
||||
mDialogLaunchAnimator.createActivityLaunchController(v);
|
||||
|
||||
if (controller == null) {
|
||||
holder.mDialog.dismiss();
|
||||
}
|
||||
|
||||
mActivityStarter
|
||||
.postStartActivityDismissingKeyguard(getLongClickIntent(), 0);
|
||||
.postStartActivityDismissingKeyguard(getLongClickIntent(), 0,
|
||||
controller);
|
||||
});
|
||||
holder.init(dialog);
|
||||
SystemUIDialog.setShowForAllUsers(dialog, true);
|
||||
|
||||
@@ -164,7 +164,7 @@ public class InternetAdapter extends RecyclerView.Adapter<InternetAdapter.Intern
|
||||
if (connectedState != WifiEntry.CONNECTED_STATE_DISCONNECTED) {
|
||||
mWifiListLayout.setOnClickListener(
|
||||
v -> mInternetDialogController.launchWifiNetworkDetailsSetting(
|
||||
wifiEntry.getKey()));
|
||||
wifiEntry.getKey(), v));
|
||||
return;
|
||||
}
|
||||
mWifiListLayout.setOnClickListener(v -> {
|
||||
|
||||
@@ -355,8 +355,8 @@ public class InternetDialog extends SystemUIDialog implements
|
||||
isChecked, false);
|
||||
}
|
||||
});
|
||||
mConnectedWifListLayout.setOnClickListener(v -> onClickConnectedWifi());
|
||||
mSeeAllLayout.setOnClickListener(v -> onClickSeeMoreButton());
|
||||
mConnectedWifListLayout.setOnClickListener(this::onClickConnectedWifi);
|
||||
mSeeAllLayout.setOnClickListener(this::onClickSeeMoreButton);
|
||||
mWiFiToggle.setOnCheckedChangeListener(
|
||||
(buttonView, isChecked) -> {
|
||||
if (mWifiManager == null) return;
|
||||
@@ -519,7 +519,7 @@ public class InternetDialog extends SystemUIDialog implements
|
||||
if (TextUtils.isEmpty(mWifiScanNotifyText.getText())) {
|
||||
final AnnotationLinkSpan.LinkInfo linkInfo = new AnnotationLinkSpan.LinkInfo(
|
||||
AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION,
|
||||
v -> mInternetDialogController.launchWifiScanningSetting());
|
||||
mInternetDialogController::launchWifiScanningSetting);
|
||||
mWifiScanNotifyText.setText(AnnotationLinkSpan.linkify(
|
||||
getContext().getText(R.string.wifi_scan_notify_message), linkInfo));
|
||||
mWifiScanNotifyText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
@@ -527,15 +527,16 @@ public class InternetDialog extends SystemUIDialog implements
|
||||
mWifiScanNotifyLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
void onClickConnectedWifi() {
|
||||
void onClickConnectedWifi(View view) {
|
||||
if (mConnectedWifiEntry == null) {
|
||||
return;
|
||||
}
|
||||
mInternetDialogController.launchWifiNetworkDetailsSetting(mConnectedWifiEntry.getKey());
|
||||
mInternetDialogController.launchWifiNetworkDetailsSetting(mConnectedWifiEntry.getKey(),
|
||||
view);
|
||||
}
|
||||
|
||||
void onClickSeeMoreButton() {
|
||||
mInternetDialogController.launchNetworkSetting();
|
||||
void onClickSeeMoreButton(View view) {
|
||||
mInternetDialogController.launchNetworkSetting(view);
|
||||
}
|
||||
|
||||
CharSequence getDialogTitleText() {
|
||||
|
||||
@@ -72,6 +72,7 @@ import com.android.settingslib.mobile.TelephonyIcons;
|
||||
import com.android.settingslib.net.SignalStrengthUtil;
|
||||
import com.android.settingslib.wifi.WifiUtils;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator;
|
||||
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||
import com.android.systemui.dagger.qualifiers.Background;
|
||||
@@ -620,36 +621,32 @@ public class InternetDialogController implements AccessPointController.AccessPoi
|
||||
return summary;
|
||||
}
|
||||
|
||||
void launchNetworkSetting() {
|
||||
// Dismissing a dialog into its touch surface and starting an activity at the same time
|
||||
// looks bad, so let's make sure the dialog just fades out quickly.
|
||||
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
|
||||
mCallback.dismissDialog();
|
||||
private void startActivity(Intent intent, View view) {
|
||||
ActivityLaunchAnimator.Controller controller =
|
||||
mDialogLaunchAnimator.createActivityLaunchController(view);
|
||||
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(getSettingsIntent(), 0);
|
||||
if (controller == null) {
|
||||
mCallback.dismissDialog();
|
||||
}
|
||||
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0, controller);
|
||||
}
|
||||
|
||||
void launchWifiNetworkDetailsSetting(String key) {
|
||||
void launchNetworkSetting(View view) {
|
||||
startActivity(getSettingsIntent(), view);
|
||||
}
|
||||
|
||||
void launchWifiNetworkDetailsSetting(String key, View view) {
|
||||
Intent intent = getWifiDetailsSettingsIntent(key);
|
||||
if (intent != null) {
|
||||
// Dismissing a dialog into its touch surface and starting an activity at the same time
|
||||
// looks bad, so let's make sure the dialog just fades out quickly.
|
||||
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
|
||||
mCallback.dismissDialog();
|
||||
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
|
||||
startActivity(intent, view);
|
||||
}
|
||||
}
|
||||
|
||||
void launchWifiScanningSetting() {
|
||||
// Dismissing a dialog into its touch surface and starting an activity at the same time
|
||||
// looks bad, so let's make sure the dialog just fades out quickly.
|
||||
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
|
||||
mCallback.dismissDialog();
|
||||
|
||||
void launchWifiScanningSetting(View view) {
|
||||
final Intent intent = new Intent(ACTION_WIFI_SCANNING_SETTINGS);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
|
||||
startActivity(intent, view);
|
||||
}
|
||||
|
||||
void connectCarrierNetwork() {
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.android.systemui.qs.user
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.DialogInterface.BUTTON_NEUTRAL
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
@@ -84,16 +85,20 @@ class UserSwitchDialogController @VisibleForTesting constructor(
|
||||
setPositiveButton(R.string.quick_settings_done) { _, _ ->
|
||||
uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_CLOSE)
|
||||
}
|
||||
setNeutralButton(R.string.quick_settings_more_user_settings) { _, _ ->
|
||||
setNeutralButton(R.string.quick_settings_more_user_settings, { _, _ ->
|
||||
if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
|
||||
uiEventLogger.log(QSUserSwitcherEvent.QS_USER_MORE_SETTINGS)
|
||||
val controller = dialogLaunchAnimator.createActivityLaunchController(
|
||||
getButton(BUTTON_NEUTRAL))
|
||||
|
||||
if (controller == null) {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
activityStarter.postStartActivityDismissingKeyguard(
|
||||
USER_SETTINGS_INTENT,
|
||||
0
|
||||
)
|
||||
USER_SETTINGS_INTENT, 0, controller)
|
||||
}
|
||||
}
|
||||
}, false /* dismissOnClick */)
|
||||
val gridFrame = LayoutInflater.from(this.context)
|
||||
.inflate(R.layout.qs_user_dialog_content, null)
|
||||
setView(gridFrame)
|
||||
|
||||
@@ -2580,9 +2580,9 @@ public class StatusBar extends CoreStartable implements
|
||||
return controllerFromStatusBar.get();
|
||||
}
|
||||
|
||||
if (dismissShade && rootView == mNotificationShadeWindowView) {
|
||||
// We are animating a view in the shade. We have to make sure that we collapse it when
|
||||
// the animation ends or is cancelled.
|
||||
if (dismissShade) {
|
||||
// If the view is not in the status bar, then we are animating a view in the shade.
|
||||
// We have to make sure that we collapse it when the animation ends or is cancelled.
|
||||
return new StatusBarLaunchAnimatorController(animationController, this,
|
||||
true /* isLaunchForActivity */);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.android.systemui.statusbar.phone
|
||||
|
||||
import android.view.View
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.animation.LaunchAnimator
|
||||
|
||||
@@ -12,6 +13,11 @@ class StatusBarLaunchAnimatorController(
|
||||
private val statusBar: StatusBar,
|
||||
private val isLaunchForActivity: Boolean = true
|
||||
) : ActivityLaunchAnimator.Controller by delegate {
|
||||
// Always sync the opening window with the shade, given that we draw a hole punch in the shade
|
||||
// of the same size and position as the opening app to make it visible.
|
||||
override val openingWindowSyncView: View?
|
||||
get() = statusBar.notificationShadeWindowView
|
||||
|
||||
override fun onIntentStarted(willAnimate: Boolean) {
|
||||
delegate.onIntentStarted(willAnimate)
|
||||
if (!willAnimate) {
|
||||
|
||||
@@ -44,6 +44,9 @@ import com.android.systemui.Dependency;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base class for dialogs that should appear over panels and keyguard.
|
||||
*
|
||||
@@ -68,6 +71,8 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh
|
||||
private int mLastConfigurationWidthDp = -1;
|
||||
private int mLastConfigurationHeightDp = -1;
|
||||
|
||||
private List<Runnable> mOnCreateRunnables = new ArrayList<>();
|
||||
|
||||
public SystemUIDialog(Context context) {
|
||||
this(context, R.style.Theme_SystemUI_Dialog);
|
||||
}
|
||||
@@ -110,6 +115,10 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh
|
||||
mLastConfigurationWidthDp = config.screenWidthDp;
|
||||
mLastConfigurationHeightDp = config.screenHeightDp;
|
||||
updateWindowSize();
|
||||
|
||||
for (int i = 0; i < mOnCreateRunnables.size(); i++) {
|
||||
mOnCreateRunnables.get(i).run();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWindowSize() {
|
||||
@@ -197,16 +206,67 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh
|
||||
setMessage(mContext.getString(resId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener to be invoked when the positive button of the dialog is pressed. The dialog
|
||||
* will automatically be dismissed when the button is clicked.
|
||||
*/
|
||||
public void setPositiveButton(int resId, OnClickListener onClick) {
|
||||
setButton(BUTTON_POSITIVE, mContext.getString(resId), onClick);
|
||||
setPositiveButton(resId, onClick, true /* dismissOnClick */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener to be invoked when the positive button of the dialog is pressed. The dialog
|
||||
* will be dismissed when the button is clicked iff {@code dismissOnClick} is true.
|
||||
*/
|
||||
public void setPositiveButton(int resId, OnClickListener onClick, boolean dismissOnClick) {
|
||||
setButton(BUTTON_POSITIVE, resId, onClick, dismissOnClick);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener to be invoked when the negative button of the dialog is pressed. The dialog
|
||||
* will automatically be dismissed when the button is clicked.
|
||||
*/
|
||||
public void setNegativeButton(int resId, OnClickListener onClick) {
|
||||
setButton(BUTTON_NEGATIVE, mContext.getString(resId), onClick);
|
||||
setNegativeButton(resId, onClick, true /* dismissOnClick */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener to be invoked when the negative button of the dialog is pressed. The dialog
|
||||
* will be dismissed when the button is clicked iff {@code dismissOnClick} is true.
|
||||
*/
|
||||
public void setNegativeButton(int resId, OnClickListener onClick, boolean dismissOnClick) {
|
||||
setButton(BUTTON_NEGATIVE, resId, onClick, dismissOnClick);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener to be invoked when the neutral button of the dialog is pressed. The dialog
|
||||
* will automatically be dismissed when the button is clicked.
|
||||
*/
|
||||
public void setNeutralButton(int resId, OnClickListener onClick) {
|
||||
setButton(BUTTON_NEUTRAL, mContext.getString(resId), onClick);
|
||||
setNeutralButton(resId, onClick, true /* dismissOnClick */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener to be invoked when the neutral button of the dialog is pressed. The dialog
|
||||
* will be dismissed when the button is clicked iff {@code dismissOnClick} is true.
|
||||
*/
|
||||
public void setNeutralButton(int resId, OnClickListener onClick, boolean dismissOnClick) {
|
||||
setButton(BUTTON_NEUTRAL, resId, onClick, dismissOnClick);
|
||||
}
|
||||
|
||||
private void setButton(int whichButton, int resId, OnClickListener onClick,
|
||||
boolean dismissOnClick) {
|
||||
if (dismissOnClick) {
|
||||
setButton(whichButton, mContext.getString(resId), onClick);
|
||||
} else {
|
||||
// Set a null OnClickListener to make sure the button is still created and shown.
|
||||
setButton(whichButton, mContext.getString(resId), (OnClickListener) null);
|
||||
|
||||
// When the dialog is created, set the click listener but don't dismiss the dialog when
|
||||
// it is clicked.
|
||||
mOnCreateRunnables.add(() -> getButton(whichButton).setOnClickListener(
|
||||
view -> onClick.onClick(this, whichButton)));
|
||||
}
|
||||
}
|
||||
|
||||
public static void setShowForAllUsers(Dialog dialog, boolean show) {
|
||||
|
||||
@@ -46,7 +46,7 @@ import kotlin.concurrent.thread
|
||||
@RunWithLooper
|
||||
class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
private val launchContainer = LinearLayout(mContext)
|
||||
private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
|
||||
private val testLaunchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
|
||||
@Mock lateinit var callback: ActivityLaunchAnimator.Callback
|
||||
@Mock lateinit var listener: ActivityLaunchAnimator.Listener
|
||||
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
|
||||
@@ -58,7 +58,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
activityLaunchAnimator = ActivityLaunchAnimator(launchAnimator)
|
||||
activityLaunchAnimator = ActivityLaunchAnimator(testLaunchAnimator, testLaunchAnimator)
|
||||
activityLaunchAnimator.callback = callback
|
||||
activityLaunchAnimator.addListener(listener)
|
||||
}
|
||||
@@ -129,13 +129,11 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
|
||||
`when`(callback.isOnKeyguard()).thenReturn(true)
|
||||
val animator = ActivityLaunchAnimator(launchAnimator)
|
||||
animator.callback = callback
|
||||
|
||||
val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
|
||||
var animationAdapter: RemoteAnimationAdapter? = null
|
||||
|
||||
startIntentWithAnimation(animator) { adapter ->
|
||||
startIntentWithAnimation(activityLaunchAnimator) { adapter ->
|
||||
animationAdapter = adapter
|
||||
ActivityManager.START_DELIVERED_TO_TOP
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.android.systemui.SysuiTestCase
|
||||
import junit.framework.Assert.assertEquals
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertNotNull
|
||||
import junit.framework.Assert.assertNull
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@@ -43,7 +44,7 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
dialogLaunchAnimator = DialogLaunchAnimator(
|
||||
dreamManager, launchAnimator, forceDisableSynchronization = true)
|
||||
dreamManager, launchAnimator, isForTesting = true)
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -92,11 +93,6 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
|
||||
|
||||
// Clicking the transparent background should dismiss the dialog.
|
||||
runOnMainThreadAndWaitForIdleSync {
|
||||
// TODO(b/204561691): Remove this call to disableAllCurrentDialogsExitAnimations() and
|
||||
// make sure that the test still pass on git_master/cf_x86_64_phone-userdebug in
|
||||
// Forrest.
|
||||
dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
|
||||
|
||||
transparentBackground.performClick()
|
||||
}
|
||||
assertFalse(dialog.isShowing)
|
||||
@@ -110,7 +106,6 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
|
||||
assertTrue(firstDialog.isShowing)
|
||||
assertTrue(secondDialog.isShowing)
|
||||
runOnMainThreadAndWaitForIdleSync {
|
||||
dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
|
||||
dialogLaunchAnimator.dismissStack(secondDialog)
|
||||
}
|
||||
|
||||
@@ -118,6 +113,38 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
|
||||
assertFalse(secondDialog.isShowing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testActivityLaunchControllerFromDialog() {
|
||||
val firstDialog = createAndShowDialog()
|
||||
val secondDialog = createDialogAndShowFromDialog(firstDialog)
|
||||
|
||||
val controller =
|
||||
dialogLaunchAnimator.createActivityLaunchController(secondDialog.contentView)!!
|
||||
|
||||
// The dialog shouldn't be dismissable during the animation.
|
||||
runOnMainThreadAndWaitForIdleSync {
|
||||
controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
|
||||
secondDialog.dismiss()
|
||||
}
|
||||
assertTrue(secondDialog.isShowing)
|
||||
|
||||
// Both dialogs should be dismissed at the end of the animation.
|
||||
runOnMainThreadAndWaitForIdleSync {
|
||||
controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
|
||||
}
|
||||
assertFalse(firstDialog.isShowing)
|
||||
assertFalse(secondDialog.isShowing)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testActivityLaunchFromHiddenDialog() {
|
||||
val dialog = createAndShowDialog()
|
||||
runOnMainThreadAndWaitForIdleSync {
|
||||
dialog.hide()
|
||||
}
|
||||
assertNull(dialogLaunchAnimator.createActivityLaunchController(dialog.contentView))
|
||||
}
|
||||
|
||||
private fun createAndShowDialog(): TestDialog {
|
||||
return runOnMainThreadAndWaitForIdleSync {
|
||||
val touchSurfaceRoot = LinearLayout(context)
|
||||
|
||||
@@ -59,6 +59,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
|
||||
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
|
||||
private Icon mIcon = mock(Icon.class);
|
||||
private IconCompat mIconCompat = mock(IconCompat.class);
|
||||
private View mDialogLaunchView = mock(View.class);
|
||||
|
||||
private MediaOutputAdapter mMediaOutputAdapter;
|
||||
private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
|
||||
@@ -245,7 +246,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
|
||||
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
|
||||
mViewHolder.mContainerLayout.performClick();
|
||||
|
||||
verify(mMediaOutputController).launchBluetoothPairing();
|
||||
verify(mMediaOutputController).launchBluetoothPairing(mViewHolder.mContainerLayout);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -136,6 +136,8 @@ public class InternetDialogControllerTest extends SysuiTestCase {
|
||||
private LocationController mLocationController;
|
||||
@Mock
|
||||
private DialogLaunchAnimator mDialogLaunchAnimator;
|
||||
@Mock
|
||||
private View mDialogLaunchView;
|
||||
|
||||
private TestableResources mTestableResources;
|
||||
private InternetDialogController mInternetDialogController;
|
||||
@@ -384,7 +386,8 @@ public class InternetDialogControllerTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void launchWifiNetworkDetailsSetting_withNoWifiEntryKey_doNothing() {
|
||||
mInternetDialogController.launchWifiNetworkDetailsSetting(null /* key */);
|
||||
mInternetDialogController.launchWifiNetworkDetailsSetting(null /* key */,
|
||||
mDialogLaunchView);
|
||||
|
||||
verify(mActivityStarter, never())
|
||||
.postStartActivityDismissingKeyguard(any(Intent.class), anyInt());
|
||||
@@ -392,9 +395,11 @@ public class InternetDialogControllerTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void launchWifiNetworkDetailsSetting_withWifiEntryKey_startActivity() {
|
||||
mInternetDialogController.launchWifiNetworkDetailsSetting("wifi_entry_key");
|
||||
mInternetDialogController.launchWifiNetworkDetailsSetting("wifi_entry_key",
|
||||
mDialogLaunchView);
|
||||
|
||||
verify(mActivityStarter).postStartActivityDismissingKeyguard(any(Intent.class), anyInt());
|
||||
verify(mActivityStarter).postStartActivityDismissingKeyguard(any(Intent.class), anyInt(),
|
||||
any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -458,7 +458,8 @@ public class InternetDialogTest extends SysuiTestCase {
|
||||
public void onClickSeeMoreButton_clickSeeAll_verifyLaunchNetworkSetting() {
|
||||
mSeeAll.performClick();
|
||||
|
||||
verify(mInternetDialogController).launchNetworkSetting();
|
||||
verify(mInternetDialogController).launchNetworkSetting(
|
||||
mDialogView.requireViewById(R.id.see_all_layout));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.logging.UiEventLogger
|
||||
import com.android.systemui.SysuiTestCase
|
||||
@@ -63,6 +64,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
|
||||
@Mock
|
||||
private lateinit var launchView: View
|
||||
@Mock
|
||||
private lateinit var neutralButton: Button
|
||||
@Mock
|
||||
private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
|
||||
@Mock
|
||||
private lateinit var uiEventLogger: UiEventLogger
|
||||
@@ -130,14 +133,17 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
|
||||
|
||||
controller.showDialog(launchView)
|
||||
|
||||
verify(dialog).setNeutralButton(anyInt(), capture(clickCaptor))
|
||||
verify(dialog)
|
||||
.setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
|
||||
`when`(dialog.getButton(DialogInterface.BUTTON_NEUTRAL)).thenReturn(neutralButton)
|
||||
|
||||
clickCaptor.value.onClick(dialog, DialogInterface.BUTTON_NEUTRAL)
|
||||
|
||||
verify(activityStarter)
|
||||
.postStartActivityDismissingKeyguard(
|
||||
argThat(IntentMatcher(Settings.ACTION_USER_SETTINGS)),
|
||||
eq(0)
|
||||
eq(0),
|
||||
eq(null)
|
||||
)
|
||||
verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_MORE_SETTINGS)
|
||||
}
|
||||
@@ -148,7 +154,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
|
||||
|
||||
controller.showDialog(launchView)
|
||||
|
||||
verify(dialog).setNeutralButton(anyInt(), capture(clickCaptor))
|
||||
verify(dialog)
|
||||
.setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
|
||||
|
||||
clickCaptor.value.onClick(dialog, DialogInterface.BUTTON_NEUTRAL)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user