Merge changes from topics "dialog-launch", "launch-animator" into sc-v2-dev

* changes:
  Animate media dialogs
  Introduce DialogLaunchAnimator (1/2)
  Split ActivityLaunchAnimator (1/2)
This commit is contained in:
Jordan Demeulenaere
2021-09-29 11:58:24 +00:00
committed by Android (Google) Code Review
44 changed files with 1621 additions and 430 deletions

View File

@@ -165,6 +165,10 @@ public final class MathUtils {
return start + (stop - start) * amount; return start + (stop - start) * amount;
} }
public static float lerp(int start, int stop, float amount) {
return lerp((float) start, (float) stop, amount);
}
/** /**
* Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link * Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link
* #lerp}{@code (a, b, s)} * #lerp}{@code (a, b, s)}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<!-- The enter animation of the host dialog is a translation of 0px that lasts 500ms so that the -->
<!-- host dialog is directly visible but the dim background still takes 500ms to fade in. -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0"
android:toXDelta="0"
android:duration="500" />

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<alpha
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/decelerate_cubic"
android:duration="150"
android:fromAlpha="1.0"
android:toAlpha="0.0" />

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<item type="id" name="launch_animation_running"/>
</resources>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<style name="HostDialogTheme">
<item name="android:windowAnimationStyle">@style/Animation.HostDialog</item>
<item name="android:windowIsFloating">false</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="Animation.HostDialog" parent="@android:style/Animation">
<item name="android:windowEnterAnimation">@anim/launch_host_dialog_enter</item>
<item name="android:windowExitAnimation">@anim/launch_host_dialog_exit</item>
</style>
</resources>

View File

@@ -1,23 +1,31 @@
/*
* 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 package com.android.systemui.animation
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.app.ActivityManager import android.app.ActivityManager
import android.app.ActivityTaskManager import android.app.ActivityTaskManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.TaskInfo import android.app.TaskInfo
import android.content.Context
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.graphics.drawable.GradientDrawable
import android.os.Looper import android.os.Looper
import android.os.RemoteException import android.os.RemoteException
import android.util.Log import android.util.Log
import android.util.MathUtils
import android.view.IRemoteAnimationFinishedCallback import android.view.IRemoteAnimationFinishedCallback
import android.view.IRemoteAnimationRunner import android.view.IRemoteAnimationRunner
import android.view.RemoteAnimationAdapter import android.view.RemoteAnimationAdapter
@@ -26,7 +34,6 @@ import android.view.SyncRtSurfaceTransactionApplier
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import android.view.animation.AnimationUtils
import android.view.animation.PathInterpolator import android.view.animation.PathInterpolator
import com.android.internal.annotations.VisibleForTesting import com.android.internal.annotations.VisibleForTesting
import com.android.internal.policy.ScreenDecorationsUtils import com.android.internal.policy.ScreenDecorationsUtils
@@ -38,52 +45,23 @@ 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.
*/ */
class ActivityLaunchAnimator( class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) {
private val callback: Callback,
context: Context
) {
companion object { companion object {
private const val DEBUG = false
const val ANIMATION_DURATION = 500L
private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L
private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT
private const val ANIMATION_DURATION_NAV_FADE_IN = 266L private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
private const val ANIMATION_DELAY_NAV_FADE_IN = private const val ANIMATION_DELAY_NAV_FADE_IN =
ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN LaunchAnimator.ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
private const val LAUNCH_TIMEOUT = 1000L private const val LAUNCH_TIMEOUT = 1000L
@JvmField val CONTENT_FADE_OUT_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f)
private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f) private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f) private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
/**
* Given the [linearProgress] of a launch animation, return the linear progress of the
* sub-animation starting [delay] ms after the launch animation and that lasts [duration].
*/
@JvmStatic
fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
return MathUtils.constrain(
(linearProgress * ANIMATION_DURATION - delay) / duration,
0.0f,
1.0f
)
}
} }
/** The interpolator used for the width, height, Y position and corner radius. */ /**
private val animationInterpolator = AnimationUtils.loadInterpolator(context, * The callback of this animator. This should be set before any call to
R.interpolator.launch_animation_interpolator_y) * [start(Pending)IntentWithAnimation].
*/
/** The interpolator used for the X position. */ var callback: Callback? = null
private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
R.interpolator.launch_animation_interpolator_x)
private val cornerRadii = FloatArray(8)
/** /**
* Start an intent and animate the opening window. The intent will be started by running * Start an intent and animate the opening window. The intent will be started by running
@@ -119,6 +97,8 @@ class ActivityLaunchAnimator(
return return
} }
val callback = this.callback ?: throw IllegalStateException(
"ActivityLaunchAnimator.callback must be set before using this animator")
val runner = Runner(controller) val runner = Runner(controller)
val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen
@@ -126,9 +106,9 @@ class ActivityLaunchAnimator(
// keyguard with the animation // keyguard with the animation
val animationAdapter = if (!hideKeyguardWithAnimation) { val animationAdapter = if (!hideKeyguardWithAnimation) {
RemoteAnimationAdapter( RemoteAnimationAdapter(
runner, runner,
ANIMATION_DURATION, LaunchAnimator.ANIMATION_DURATION,
ANIMATION_DURATION - 150 /* statusBarTransitionDelay */ LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
) )
} else { } else {
null null
@@ -150,10 +130,10 @@ class ActivityLaunchAnimator(
// Only animate if the app is not already on top and will be opened, unless we are on the // Only animate if the app is not already on top and will be opened, unless we are on the
// keyguard. // keyguard.
val willAnimate = val willAnimate =
launchResult == ActivityManager.START_TASK_TO_FRONT || launchResult == ActivityManager.START_TASK_TO_FRONT ||
launchResult == ActivityManager.START_SUCCESS || launchResult == ActivityManager.START_SUCCESS ||
(launchResult == ActivityManager.START_DELIVERED_TO_TOP && (launchResult == ActivityManager.START_DELIVERED_TO_TOP &&
hideKeyguardWithAnimation) hideKeyguardWithAnimation)
Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " + Log.i(TAG, "launchResult=$launchResult willAnimate=$willAnimate " +
"hideKeyguardWithAnimation=$hideKeyguardWithAnimation") "hideKeyguardWithAnimation=$hideKeyguardWithAnimation")
@@ -234,7 +214,7 @@ class ActivityLaunchAnimator(
* *
* Note that all callbacks (onXXX methods) are all called on the main thread. * Note that all callbacks (onXXX methods) are all called on the main thread.
*/ */
interface Controller { interface Controller : LaunchAnimator.Controller {
companion object { companion object {
/** /**
* 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.
@@ -258,53 +238,12 @@ class ActivityLaunchAnimator(
} }
} }
/**
* The container in which the view that started the intent will be animating together with
* the opening window.
*
* This will be used to:
* - Get the associated [Context].
* - Compute whether we are expanding fully above the current window.
* - Apply surface transactions in sync with RenderThread.
*
* 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
* animation.
*/
var launchContainer: ViewGroup
/**
* Return the [State] of the view that will be animated. We will animate from this state to
* the final window state.
*
* Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
* animation.
*/
fun createAnimatorState(): State
/** /**
* The intent was started. If [willAnimate] is false, nothing else will happen and the * The intent was started. If [willAnimate] is false, nothing else will happen and the
* animation will not be started. * animation will not be started.
*/ */
fun onIntentStarted(willAnimate: Boolean) {} fun onIntentStarted(willAnimate: Boolean) {}
/**
* The animation started. This is typically used to initialize any additional resource
* needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
* fully above the [root view][getRootView].
*/
fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
/** The animation made progress and the expandable view [state] should be updated. */
fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
/**
* The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
* called previously. This is typically used to clean up the resources initialized when the
* animation was started.
*/
fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
/** /**
* The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after * The animation was cancelled. Note that [onLaunchAnimationEnd] will still be called after
* this if the animation was already started, i.e. if [onLaunchAnimationStart] was called * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
@@ -313,66 +252,11 @@ class ActivityLaunchAnimator(
fun onLaunchAnimationCancelled() {} fun onLaunchAnimationCancelled() {}
} }
/** The state of an expandable view during an [ActivityLaunchAnimator] animation. */
open class State(
/** The position of the view in screen space coordinates. */
var top: Int,
var bottom: Int,
var left: Int,
var right: Int,
var topCornerRadius: Float = 0f,
var bottomCornerRadius: Float = 0f
) {
private val startTop = top
private val startBottom = bottom
private val startLeft = left
private val startRight = right
private val startWidth = width
private val startHeight = height
val startCenterX = centerX
val startCenterY = centerY
val width: Int
get() = right - left
val height: Int
get() = bottom - top
open val topChange: Int
get() = top - startTop
open val bottomChange: Int
get() = bottom - startBottom
val leftChange: Int
get() = left - startLeft
val rightChange: Int
get() = right - startRight
val widthRatio: Float
get() = width.toFloat() / startWidth
val heightRatio: Float
get() = height.toFloat() / startHeight
val centerX: Float
get() = left + width / 2f
val centerY: Float
get() = top + height / 2f
/** Whether the expanded view should be visible or hidden. */
var visible: Boolean = true
}
@VisibleForTesting @VisibleForTesting
inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() { inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
private val launchContainer = controller.launchContainer private val launchContainer = controller.launchContainer
private val context = launchContainer.context private val context = launchContainer.context
private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer) private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer)
private var animator: ValueAnimator? = null
private val matrix = Matrix() private val matrix = Matrix()
private val invertMatrix = Matrix() private val invertMatrix = Matrix()
@@ -380,6 +264,7 @@ class ActivityLaunchAnimator(
private var windowCropF = RectF() private var windowCropF = RectF()
private var timedOut = false private var timedOut = false
private var cancelled = false private var cancelled = false
private var animation: LaunchAnimator.Animation? = null
// A timeout to cancel the remote animation if it is not started within X milliseconds after // A timeout to cancel the remote animation if it is not started within X milliseconds after
// the intent was started. // the intent was started.
@@ -429,7 +314,7 @@ class ActivityLaunchAnimator(
nonApps: Array<out RemoteAnimationTarget>?, nonApps: Array<out RemoteAnimationTarget>?,
iCallback: IRemoteAnimationFinishedCallback? iCallback: IRemoteAnimationFinishedCallback?
) { ) {
if (DEBUG) { if (LaunchAnimator.DEBUG) {
Log.d(TAG, "Remote animation started") Log.d(TAG, "Remote animation started")
} }
@@ -449,36 +334,20 @@ class ActivityLaunchAnimator(
it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
} }
// Start state.
val state = controller.createAnimatorState()
val startTop = state.top
val startBottom = state.bottom
val startLeft = state.left
val startRight = state.right
val startXCenter = (startLeft + startRight) / 2f
val startWidth = startRight - startLeft
val startTopCornerRadius = state.topCornerRadius
val startBottomCornerRadius = state.bottomCornerRadius
// End state.
val windowBounds = window.screenSpaceBounds val windowBounds = window.screenSpaceBounds
val endTop = windowBounds.top val endState = LaunchAnimator.State(
val endBottom = windowBounds.bottom top = windowBounds.top,
val endLeft = windowBounds.left bottom = windowBounds.bottom,
val endRight = windowBounds.right left = windowBounds.left,
val endXCenter = (endLeft + endRight) / 2f right = windowBounds.right
val endWidth = endRight - endLeft )
val callback = this@ActivityLaunchAnimator.callback!!
val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
// TODO(b/184121838): Ensure that we are launching on the same screen. // TODO(b/184121838): We should somehow get the top and bottom radius of the window
val rootViewLocation = launchContainer.locationOnScreen // instead of recomputing isExpandingFullyAbove here.
val isExpandingFullyAbove = endTop <= rootViewLocation[1] && val isExpandingFullyAbove =
endBottom >= rootViewLocation[1] + launchContainer.height && launchAnimator.isExpandingFullyAbove(controller.launchContainer, endState)
endLeft <= rootViewLocation[0] &&
endRight >= rootViewLocation[0] + launchContainer.width
// TODO(b/184121838): We should somehow get the top and bottom radius of the window.
val endRadius = if (isExpandingFullyAbove) { val endRadius = if (isExpandingFullyAbove) {
// Most of the time, expanding fully above the root view means expanding in full // Most of the time, expanding fully above the root view means expanding in full
// screen. // screen.
@@ -488,97 +357,40 @@ class ActivityLaunchAnimator(
// a radius of 0. // a radius of 0.
0f 0f
} }
endState.topCornerRadius = endRadius
endState.bottomCornerRadius = endRadius
// We add an extra layer with the same color as the app splash screen background color, // We animate the opening window and delegate the view expansion to [this.controller].
// which is usually the same color of the app background. We first fade in this layer val delegate = this.controller
// to hide the expanding view, then we fade it out with SRC mode to draw a hole in the val controller = object : LaunchAnimator.Controller by delegate {
// launch container and reveal the opening window. override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
val windowBackgroundLayer = GradientDrawable().apply {
setColor(windowBackgroundColor)
alpha = 0
}
// Update state.
val animator = ValueAnimator.ofFloat(0f, 1f)
this.animator = animator
animator.duration = ANIMATION_DURATION
animator.interpolator = Interpolators.LINEAR
val launchContainerOverlay = launchContainer.overlay
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
if (DEBUG) {
Log.d(TAG, "Animation started")
}
callback.setBlursDisabledForAppLaunch(true) callback.setBlursDisabledForAppLaunch(true)
controller.onLaunchAnimationStart(isExpandingFullyAbove) delegate.onLaunchAnimationStart(isExpandingFullyAbove)
// Add the drawable to the launch container overlay. Overlays always draw
// drawables after views, so we know that it will be drawn above any view added
// by the controller.
launchContainerOverlay.add(windowBackgroundLayer)
} }
override fun onAnimationEnd(animation: Animator?) { override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
if (DEBUG) {
Log.d(TAG, "Animation ended")
}
callback.setBlursDisabledForAppLaunch(false) callback.setBlursDisabledForAppLaunch(false)
iCallback?.invoke() iCallback?.invoke()
controller.onLaunchAnimationEnd(isExpandingFullyAbove) delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
launchContainerOverlay.remove(windowBackgroundLayer)
}
})
animator.addUpdateListener { animation ->
if (cancelled) {
return@addUpdateListener
} }
val linearProgress = animation.animatedFraction override fun onLaunchAnimationProgress(
val progress = animationInterpolator.getInterpolation(linearProgress) state: LaunchAnimator.State,
val xProgress = animationInterpolatorX.getInterpolation(linearProgress) progress: Float,
val xCenter = MathUtils.lerp(startXCenter, endXCenter, xProgress) linearProgress: Float
val halfWidth = lerp(startWidth, endWidth, progress) / 2 ) {
applyStateToWindow(window, state)
state.top = lerp(startTop, endTop, progress).roundToInt() navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
state.bottom = lerp(startBottom, endBottom, progress).roundToInt() delegate.onLaunchAnimationProgress(state, progress, linearProgress)
state.left = (xCenter - halfWidth).roundToInt()
state.right = (xCenter + halfWidth).roundToInt()
state.topCornerRadius = MathUtils.lerp(startTopCornerRadius, endRadius, progress)
state.bottomCornerRadius =
MathUtils.lerp(startBottomCornerRadius, endRadius, progress)
// The expanding view can/should be hidden once it is completely coverred by the
// windowBackgroundLayer.
state.visible =
getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1
applyStateToWindow(window, state)
applyStateToWindowBackgroundLayer(windowBackgroundLayer, state, linearProgress)
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
// If we started expanding the view, we make it 1 pixel smaller on all sides to
// avoid artefacts on the corners caused by anti-aliasing of the view background and
// the window background layer.
if (state.top != startTop && state.left != startLeft &&
state.bottom != startBottom && state.right != startRight) {
state.top += 1
state.left += 1
state.right -= 1
state.bottom -= 1
} }
controller.onLaunchAnimationProgress(state, progress, linearProgress)
} }
animator.start() // 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: State) { private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) {
val screenBounds = window.screenSpaceBounds val screenBounds = window.screenSpaceBounds
val centerX = (screenBounds.left + screenBounds.right) / 2f val centerX = (screenBounds.left + screenBounds.right) / 2f
val centerY = (screenBounds.top + screenBounds.bottom) / 2f val centerY = (screenBounds.top + screenBounds.bottom) / 2f
@@ -632,48 +444,13 @@ class ActivityLaunchAnimator(
transactionApplier.scheduleApply(params) transactionApplier.scheduleApply(params)
} }
private fun applyStateToWindowBackgroundLayer(
drawable: GradientDrawable,
state: State,
linearProgress: Float
) {
// Update position.
drawable.setBounds(state.left, state.top, state.right, state.bottom)
// Update radius.
cornerRadii[0] = state.topCornerRadius
cornerRadii[1] = state.topCornerRadius
cornerRadii[2] = state.topCornerRadius
cornerRadii[3] = state.topCornerRadius
cornerRadii[4] = state.bottomCornerRadius
cornerRadii[5] = state.bottomCornerRadius
cornerRadii[6] = state.bottomCornerRadius
cornerRadii[7] = state.bottomCornerRadius
drawable.cornerRadii = cornerRadii
// We first fade in the background layer to hide the expanding view, then fade it out
// with SRC mode to draw a hole punch in the status bar and reveal the opening window.
val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT)
if (fadeInProgress < 1) {
val alpha = CONTENT_FADE_OUT_INTERPOLATOR.getInterpolation(fadeInProgress)
drawable.alpha = (alpha * 0xFF).roundToInt()
drawable.setXfermode(null)
} else {
val fadeOutProgress = getProgress(linearProgress,
ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress)
drawable.alpha = (alpha * 0xFF).roundToInt()
drawable.setXfermode(SRC_MODE)
}
}
private fun applyStateToNavigationBar( private fun applyStateToNavigationBar(
navigationBar: RemoteAnimationTarget, navigationBar: RemoteAnimationTarget,
state: State, state: LaunchAnimator.State,
linearProgress: Float linearProgress: Float
) { ) {
val fadeInProgress = getProgress(linearProgress, ANIMATION_DELAY_NAV_FADE_IN, val fadeInProgress = LaunchAnimator.getProgress(linearProgress,
ANIMATION_DURATION_NAV_FADE_OUT) ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash) val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
if (fadeInProgress > 0) { if (fadeInProgress > 0) {
@@ -682,13 +459,13 @@ class ActivityLaunchAnimator(
0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat()) 0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat())
windowCrop.set(state.left, 0, state.right, state.height) windowCrop.set(state.left, 0, state.right, state.height)
params params
.withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress)) .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
.withMatrix(matrix) .withMatrix(matrix)
.withWindowCrop(windowCrop) .withWindowCrop(windowCrop)
.withVisibility(true) .withVisibility(true)
} else { } else {
val fadeOutProgress = getProgress(linearProgress, 0, val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0,
ANIMATION_DURATION_NAV_FADE_OUT) ANIMATION_DURATION_NAV_FADE_OUT)
params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress)) params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
} }
@@ -714,7 +491,7 @@ class ActivityLaunchAnimator(
cancelled = true cancelled = true
removeTimeout() removeTimeout()
context.mainExecutor.execute { context.mainExecutor.execute {
animator?.cancel() animation?.cancel()
controller.onLaunchAnimationCancelled() controller.onLaunchAnimationCancelled()
} }
} }
@@ -726,9 +503,5 @@ class ActivityLaunchAnimator(
e.printStackTrace() e.printStackTrace()
} }
} }
private fun lerp(start: Int, stop: Int, amount: Float): Float {
return MathUtils.lerp(start.toFloat(), stop.toFloat(), amount)
}
} }
} }

View File

@@ -1,3 +1,19 @@
/*
* 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 package com.android.systemui.animation
/** /**

View File

@@ -0,0 +1,541 @@
/*
* 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.app.Dialog
import android.content.Context
import android.graphics.Color
import android.os.Looper
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.widget.FrameLayout
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.
*
* Important: Don't forget to call [DialogLaunchAnimator.onDozeAmountChanged] when the doze amount
* changes to gracefully handle dialogs fading out when the device is dozing.
*/
class DialogLaunchAnimator(
private val context: Context,
private val launchAnimator: LaunchAnimator,
private val hostDialogProvider: HostDialogProvider
) {
private companion object {
private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
}
// TODO(b/201264644): Remove this set.
private val currentAnimations = hashSetOf<DialogLaunchAnimation>()
/**
* Show [dialog] by expanding it from [view].
*
* Caveats: When calling this function, the dialog content view will actually be stolen and
* attached to a different dialog (and thus a different window) which means that the actual
* dialog window will never be drawn. Moreover, unless [dialog] is a [ListenableDialog], you
* must call dismiss(), hide() and show() on the [Dialog] returned by this function to actually
* dismiss, hide or show the dialog.
*/
fun showFromView(dialog: Dialog, view: View): Dialog {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw IllegalStateException(
"showFromView must be called from the main thread and dialog must be created in " +
"the main thread")
}
// Make sure we don't run the launch animation from the same view twice at the same time.
if (view.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
Log.e(TAG, "Not running dialog launch animation as there is already one running")
dialog.show()
return dialog
}
view.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
val launchAnimation = DialogLaunchAnimation(
context, launchAnimator, hostDialogProvider, view,
onDialogDismissed = { currentAnimations.remove(it) }, originalDialog = dialog)
val hostDialog = launchAnimation.hostDialog
currentAnimations.add(launchAnimation)
// If the dialog is dismissed/hidden/shown, then we should actually dismiss/hide/show the
// host dialog.
if (dialog is ListenableDialog) {
dialog.addListener(object : DialogListener {
override fun onDismiss() {
dialog.removeListener(this)
hostDialog.dismiss()
}
override fun onHide() {
if (launchAnimation.ignoreNextCallToHide) {
launchAnimation.ignoreNextCallToHide = false
return
}
hostDialog.hide()
}
override fun onShow() {
hostDialog.show()
// We don't actually want to show the original dialog, so hide it.
launchAnimation.ignoreNextCallToHide = true
dialog.hide()
}
})
}
launchAnimation.start()
return hostDialog
}
/** Notify the current doze amount, to ensure that dialogs fade out when dozing. */
// TODO(b/193634619): Replace this by some mandatory constructor parameter to make sure that we
// don't forget to call this when the doze amount changes.
fun onDozeAmountChanged(amount: Float) {
currentAnimations.forEach { it.onDozeAmountChanged(amount) }
}
/**
* Ensure that all dialogs currently shown won't animate into their touch surface when
* dismissed.
*
* This is a temporary API meant to be called right before we both dismiss a dialog and start
* an activity, which currently does not look good if we animate the dialog into the touch
* surface at the same time as the activity starts.
*
* TODO(b/193634619): Remove this function and animate dialog into opening activity instead.
*/
fun disableAllCurrentDialogsExitAnimations() {
currentAnimations.forEach { it.exitAnimationDisabled = true }
}
}
interface HostDialogProvider {
/**
* Create a host dialog that will be used to host a launch animation. This host dialog must:
* 1. call [onCreateCallback] in its onCreate() method, e.g. right after calling
* super.onCreate().
* 2. call [dismissOverride] instead of doing any dismissing logic. The actual dismissing
* logic should instead be done inside the lambda passed to [dismissOverride], which will
* be called after the exit animation.
*
* See SystemUIHostDialogProvider for an example of implementation.
*/
fun createHostDialog(
context: Context,
theme: Int,
onCreateCallback: () -> Unit,
dismissOverride: (() -> Unit) -> Unit
): Dialog
}
/** A dialog to/from which we can add/remove listeners. */
interface ListenableDialog {
/** Add [listener] to the listeners. */
fun addListener(listener: DialogListener)
/** Remove [listener] from the listeners. */
fun removeListener(listener: DialogListener)
}
interface DialogListener {
/** Called when this dialog dismiss() is called. */
fun onDismiss()
/** Called when this dialog hide() is called. */
fun onHide()
/** Called when this dialog show() is called. */
fun onShow()
}
private class DialogLaunchAnimation(
private val context: Context,
private val launchAnimator: LaunchAnimator,
hostDialogProvider: HostDialogProvider,
/** The view that triggered the dialog after being tapped. */
private val touchSurface: View,
/**
* A callback that will be called with this [DialogLaunchAnimation] after the dialog was
* dismissed and the exit animation is done.
*/
private val onDialogDismissed: (DialogLaunchAnimation) -> Unit,
/** The original dialog whose content will be shown and animate in/out in [hostDialog]. */
private val originalDialog: Dialog
) {
/**
* The fullscreen dialog to which we will add the content view [originalDialogView] of
* [originalDialog].
*/
val hostDialog = hostDialogProvider.createHostDialog(
context, R.style.HostDialogTheme, this::onHostDialogCreated, this::onHostDialogDismissed)
/** The root content view of [hostDialog]. */
private val hostDialogRoot = FrameLayout(context)
/**
* The content view of [originalDialog], which will be stolen from that dialog and added to
* [hostDialogRoot].
*/
private var originalDialogView: View? = null
/**
* The background color of [originalDialogView], taking into consideration the [originalDialog]
* window background color.
*/
private var originalDialogBackgroundColor = Color.BLACK
/**
* Whether we are currently launching/showing the dialog by animating it from [touchSurface].
*/
private var isLaunching = true
/** Whether we are currently dismissing/hiding the dialog by animating into [touchSurface]. */
private var isDismissing = false
private var dismissRequested = false
private var drawHostDialog = false
var ignoreNextCallToHide = false
var exitAnimationDisabled = false
fun start() {
// Show the host (fullscreen) dialog, to which we will add the stolen dialog view.
hostDialog.show()
// Steal the dialog view. We do that by showing it but preventing it from drawing, then
// hiding it as soon as its content is available.
stealOriginalDialogContentView(then = this::showDialogFromView)
}
private fun onHostDialogCreated() {
// Make the dialog fullscreen with a transparent background.
hostDialog.setContentView(
hostDialogRoot,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
val window = hostDialog.window
?: throw IllegalStateException("There is no window associated to the host dialog")
window.setBackgroundDrawableResource(android.R.color.transparent)
window.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
// Prevent the host dialog from drawing until the animation starts.
hostDialogRoot.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (drawHostDialog) {
hostDialogRoot.viewTreeObserver.removeOnPreDrawListener(this)
return true
}
return false
}
}
)
}
/** Get the content view of [originalDialog] and pass it to [then]. */
private fun stealOriginalDialogContentView(then: (View) -> Unit) {
// The original dialog content view will be attached to android.R.id.content when the dialog
// is shown, so we show the dialog and add an observer to get the view but also prevents the
// original dialog from being drawn.
val androidContent = originalDialog.findViewById<ViewGroup>(android.R.id.content)
?: throw IllegalStateException("Dialog does not have any android.R.id.content view")
androidContent.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (androidContent.childCount == 1) {
androidContent.viewTreeObserver.removeOnPreDrawListener(this)
// Hide the animated dialog. Because of the dialog listener set up
// earlier, this would also hide the host dialog, but in this case we
// need to keep the host dialog visible.
ignoreNextCallToHide = true
originalDialog.hide()
then(androidContent.getChildAt(0))
return false
}
// Never draw the original dialog content.
return false
}
})
originalDialog.show()
}
private fun showDialogFromView(dialogView: View) {
// Save the dialog view for later as we will need it for the close animation.
this.originalDialogView = dialogView
// Close the dialog when clicking outside of it.
hostDialogRoot.setOnClickListener { hostDialog.dismiss() }
dialogView.isClickable = true
// Set the background of the window dialog to the dialog itself.
// TODO(b/193634619): Support dialog windows without background.
// TODO(b/193634619): Support dialog whose background comes from the content view instead of
// the window.
val typedArray =
originalDialog.context.obtainStyledAttributes(com.android.internal.R.styleable.Window)
val backgroundRes =
typedArray.getResourceId(com.android.internal.R.styleable.Window_windowBackground, 0)
typedArray.recycle()
if (backgroundRes == 0) {
throw IllegalStateException("Dialogs with no backgrounds on window are not supported")
}
dialogView.setBackgroundResource(backgroundRes)
originalDialogBackgroundColor =
GhostedViewLaunchAnimatorController.findGradientDrawable(dialogView.background!!)
?.color
?.defaultColor ?: Color.BLACK
// Add the dialog view to the host (fullscreen) dialog and make it invisible to make sure
// it's not drawn yet.
(dialogView.parent as? ViewGroup)?.removeView(dialogView)
hostDialogRoot.addView(
dialogView,
// We give it the size of its original dialog window.
FrameLayout.LayoutParams(
originalDialog.window.attributes.width,
originalDialog.window.attributes.height,
Gravity.CENTER
)
)
dialogView.visibility = View.INVISIBLE
// Start the animation when the dialog is laid out in the center of the host dialog.
dialogView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
view: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
dialogView.removeOnLayoutChangeListener(this)
startAnimation(
isLaunching = true,
onLaunchAnimationStart = { drawHostDialog = true },
onLaunchAnimationEnd = {
touchSurface.setTag(R.id.launch_animation_running, null)
// We hide the touch surface when the dialog is showing. We will make this
// view visible again when dismissing the dialog.
// TODO(b/193634619): Provide an easy way for views to check if they should
// be hidden because of a dialog launch so that they don't override this
// visibility when updating/refreshing itself.
touchSurface.visibility = View.INVISIBLE
isLaunching = false
// dismiss was called during the animation, dismiss again now to actually
// dismiss.
if (dismissRequested) {
hostDialog.dismiss()
}
}
)
}
})
}
private fun onHostDialogDismissed(actualDismiss: () -> Unit) {
if (Looper.myLooper() != Looper.getMainLooper()) {
context.mainExecutor.execute { onHostDialogDismissed(actualDismiss) }
return
}
// TODO(b/193634619): Support interrupting the launch animation in the middle.
if (isLaunching) {
dismissRequested = true
return
}
if (isDismissing) {
return
}
isDismissing = true
hideDialogIntoView { instantDismiss: Boolean ->
if (instantDismiss) {
originalDialog.hide()
hostDialog.hide()
}
originalDialog.dismiss()
actualDismiss()
}
}
/**
* Hide the dialog into the touch surface and call [dismissDialogs] when the animation is done
* (passing instantDismiss=true) or if it's skipped (passing instantDismiss=false) to actually
* dismiss the dialogs.
*/
private fun hideDialogIntoView(dismissDialogs: (Boolean) -> Unit) {
if (!shouldAnimateDialogIntoView()) {
Log.i(TAG, "Skipping animation of dialog into the touch surface")
// If the view is invisible it's probably because of us, so we make it visible again.
if (touchSurface.visibility == View.INVISIBLE) {
touchSurface.visibility = View.VISIBLE
}
dismissDialogs(false /* instantDismiss */)
onDialogDismissed(this@DialogLaunchAnimation)
return
}
startAnimation(
isLaunching = false,
onLaunchAnimationStart = {
// Remove the dim background as soon as we start the animation.
hostDialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
},
onLaunchAnimationEnd = {
touchSurface.visibility = View.VISIBLE
originalDialogView!!.visibility = View.INVISIBLE
dismissDialogs(true /* instantDismiss */)
onDialogDismissed(this@DialogLaunchAnimation)
}
)
}
private fun startAnimation(
isLaunching: Boolean,
onLaunchAnimationStart: () -> Unit = {},
onLaunchAnimationEnd: () -> Unit = {}
) {
val dialogView = this.originalDialogView!!
// Create 2 ghost controllers to animate both the dialog and the touch surface in the host
// dialog.
val startView = if (isLaunching) touchSurface else dialogView
val endView = if (isLaunching) dialogView else touchSurface
val startViewController = GhostedViewLaunchAnimatorController(startView)
val endViewController = GhostedViewLaunchAnimatorController(endView)
startViewController.launchContainer = hostDialogRoot
endViewController.launchContainer = hostDialogRoot
val endState = endViewController.createAnimatorState()
val controller = object : LaunchAnimator.Controller {
override var launchContainer: ViewGroup
get() = startViewController.launchContainer
set(value) {
startViewController.launchContainer = value
endViewController.launchContainer = value
}
override fun createAnimatorState(): LaunchAnimator.State {
return startViewController.createAnimatorState()
}
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
startViewController.onLaunchAnimationStart(isExpandingFullyAbove)
endViewController.onLaunchAnimationStart(isExpandingFullyAbove)
onLaunchAnimationStart()
}
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
startViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
endViewController.onLaunchAnimationEnd(isExpandingFullyAbove)
onLaunchAnimationEnd()
}
override fun onLaunchAnimationProgress(
state: LaunchAnimator.State,
progress: Float,
linearProgress: Float
) {
startViewController.onLaunchAnimationProgress(state, progress, linearProgress)
// The end view is visible only iff the starting view is not visible.
state.visible = !state.visible
endViewController.onLaunchAnimationProgress(state, progress, linearProgress)
// If the dialog content is complex, its dimension might change during the launch
// animation. The animation end position might also change during the exit
// animation, for instance when locking the phone when the dialog is open. Therefore
// we update the end state to the new position/size. Usually the dialog dimension or
// position will change in the early frames, so changing the end state shouldn't
// really be noticeable.
endViewController.fillGhostedViewState(endState)
}
}
launchAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
}
private fun shouldAnimateDialogIntoView(): Boolean {
if (exitAnimationDisabled) {
return false
}
// The touch surface should be invisible by now, if it's not then something else changed its
// visibility and we probably don't want to run the animation.
if (touchSurface.visibility != View.INVISIBLE) {
return false
}
// If the touch surface is not attached or one of its ancestors is not visible, then we
// don't run the animation either.
if (!touchSurface.isAttachedToWindow) {
return false
}
return (touchSurface.parent as? View)?.isShown ?: true
}
internal fun onDozeAmountChanged(amount: Float) {
val alpha = Interpolators.ALPHA_OUT.getInterpolation(1 - amount)
val decorView = this.hostDialog.window?.decorView ?: return
if (decorView.hasOverlappingRendering() && alpha > 0.0f &&
alpha < 1.0f && decorView.layerType != View.LAYER_TYPE_HARDWARE) {
decorView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
}
decorView.alpha = alpha
}
}

View File

@@ -1,7 +1,24 @@
/*
* 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 package com.android.systemui.animation
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.ColorFilter import android.graphics.ColorFilter
import android.graphics.Insets
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.graphics.Rect import android.graphics.Rect
@@ -42,6 +59,7 @@ open class GhostedViewLaunchAnimatorController(
override var launchContainer = ghostedView.rootView as ViewGroup override var launchContainer = ghostedView.rootView as ViewGroup
private val launchContainerOverlay: ViewGroupOverlay private val launchContainerOverlay: ViewGroupOverlay
get() = launchContainer.overlay get() = launchContainer.overlay
private val launchContainerLocation = IntArray(2)
/** The ghost view that is drawn and animated instead of the ghosted view. */ /** The ghost view that is drawn and animated instead of the ghosted view. */
private var ghostView: GhostView? = null private var ghostView: GhostView? = null
@@ -59,8 +77,12 @@ open class GhostedViewLaunchAnimatorController(
* [backgroundView]. * [backgroundView].
*/ */
private var backgroundDrawable: WrappedDrawable? = null private var backgroundDrawable: WrappedDrawable? = null
private val backgroundInsets by lazy { getBackground()?.opticalInsets ?: Insets.NONE }
private var startBackgroundAlpha: Int = 0xFF private var startBackgroundAlpha: Int = 0xFF
private val ghostedViewLocation = IntArray(2)
private val ghostedViewState = LaunchAnimator.State()
/** /**
* Return the background of the [ghostedView]. This background will be used to draw the * Return the background of the [ghostedView]. This background will be used to draw the
* background of the background view that is expanding up to the final animation position. This * background of the background view that is expanding up to the final animation position. This
@@ -103,16 +125,24 @@ open class GhostedViewLaunchAnimatorController(
return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
} }
override fun createAnimatorState(): ActivityLaunchAnimator.State { override fun createAnimatorState(): LaunchAnimator.State {
val location = ghostedView.locationOnScreen val state = LaunchAnimator.State(
return ActivityLaunchAnimator.State(
top = location[1],
bottom = location[1] + ghostedView.height,
left = location[0],
right = location[0] + ghostedView.width,
topCornerRadius = getCurrentTopCornerRadius(), topCornerRadius = getCurrentTopCornerRadius(),
bottomCornerRadius = getCurrentBottomCornerRadius() bottomCornerRadius = getCurrentBottomCornerRadius()
) )
fillGhostedViewState(state)
return state
}
fun fillGhostedViewState(state: LaunchAnimator.State) {
// For the animation we are interested in the area that has a non transparent background,
// so we have to take the optical insets into account.
ghostedView.getLocationOnScreen(ghostedViewLocation)
val insets = backgroundInsets
state.top = ghostedViewLocation[1] + insets.top
state.bottom = ghostedViewLocation[1] + ghostedView.height - insets.bottom
state.left = ghostedViewLocation[0] + insets.left
state.right = ghostedViewLocation[0] + ghostedView.width - insets.right
} }
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -144,7 +174,7 @@ open class GhostedViewLaunchAnimatorController(
} }
override fun onLaunchAnimationProgress( override fun onLaunchAnimationProgress(
state: ActivityLaunchAnimator.State, state: LaunchAnimator.State,
progress: Float, progress: Float,
linearProgress: Float linearProgress: Float
) { ) {
@@ -162,19 +192,47 @@ open class GhostedViewLaunchAnimatorController(
return return
} }
val scale = min(state.widthRatio, state.heightRatio) // The ghost and backgrounds views were made invisible earlier. That can for instance happen
ghostViewMatrix.setValues(initialGhostViewMatrixValues) // when animating a dialog into a view.
ghostViewMatrix.postScale(scale, scale, state.startCenterX, state.startCenterY) if (ghostView.visibility == View.INVISIBLE) {
ghostView.visibility = View.VISIBLE
backgroundView.visibility = View.VISIBLE
}
fillGhostedViewState(ghostedViewState)
val leftChange = state.left - ghostedViewState.left
val rightChange = state.right - ghostedViewState.right
val topChange = state.top - ghostedViewState.top
val bottomChange = state.bottom - ghostedViewState.bottom
val widthRatio = state.width.toFloat() / ghostedViewState.width
val heightRatio = state.height.toFloat() / ghostedViewState.height
val scale = min(widthRatio, heightRatio)
launchContainer.getLocationOnScreen(launchContainerLocation)
GhostView.calculateMatrix(ghostedView, launchContainer, ghostViewMatrix)
ghostViewMatrix.postScale(
scale, scale,
ghostedViewState.centerX - launchContainerLocation[0],
ghostedViewState.centerY - launchContainerLocation[1]
)
ghostViewMatrix.postTranslate( ghostViewMatrix.postTranslate(
(state.leftChange + state.rightChange) / 2f, (leftChange + rightChange) / 2f,
(state.topChange + state.bottomChange) / 2f (topChange + bottomChange) / 2f
) )
ghostView.animationMatrix = ghostViewMatrix ghostView.animationMatrix = ghostViewMatrix
backgroundView.top = state.top // We need to take into account the background insets for the background position.
backgroundView.bottom = state.bottom val insets = backgroundInsets
backgroundView.left = state.left val topWithInsets = state.top - insets.top
backgroundView.right = state.right val leftWithInsets = state.left - insets.left
val rightWithInsets = state.right + insets.right
val bottomWithInsets = state.bottom + insets.bottom
backgroundView.top = topWithInsets - launchContainerLocation[1]
backgroundView.bottom = bottomWithInsets - launchContainerLocation[1]
backgroundView.left = leftWithInsets - launchContainerLocation[0]
backgroundView.right = rightWithInsets - launchContainerLocation[0]
val backgroundDrawable = backgroundDrawable!! val backgroundDrawable = backgroundDrawable!!
backgroundDrawable.wrapped?.let { backgroundDrawable.wrapped?.let {
@@ -207,7 +265,7 @@ open class GhostedViewLaunchAnimatorController(
* [drawable] is a [LayerDrawable], this will return the first layer that is a * [drawable] is a [LayerDrawable], this will return the first layer that is a
* [GradientDrawable]. * [GradientDrawable].
*/ */
private fun findGradientDrawable(drawable: Drawable): GradientDrawable? { fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
if (drawable is GradientDrawable) { if (drawable is GradientDrawable) {
return drawable return drawable
} }

View File

@@ -0,0 +1,355 @@
/*
* 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.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.drawable.GradientDrawable
import android.util.Log
import android.util.MathUtils
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnimationUtils
import android.view.animation.PathInterpolator
import kotlin.math.roundToInt
private const val TAG = "LaunchAnimator"
/** A base class to animate a window launch (activity or dialog) from a view . */
class LaunchAnimator @JvmOverloads constructor(
context: Context,
private val isForTesting: Boolean = false
) {
companion object {
internal const val DEBUG = false
const val ANIMATION_DURATION = 500L
private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L
private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT
private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f)
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
/**
* Given the [linearProgress] of a launch animation, return the linear progress of the
* sub-animation starting [delay] ms after the launch animation and that lasts [duration].
*/
@JvmStatic
fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
return MathUtils.constrain(
(linearProgress * ANIMATION_DURATION - delay) / duration,
0.0f,
1.0f
)
}
}
/** The interpolator used for the width, height, Y position and corner radius. */
private val animationInterpolator = AnimationUtils.loadInterpolator(context,
R.interpolator.launch_animation_interpolator_y)
/** The interpolator used for the X position. */
private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
R.interpolator.launch_animation_interpolator_x)
private val launchContainerLocation = IntArray(2)
private val cornerRadii = FloatArray(8)
/**
* A controller that takes care of applying the animation to an expanding view.
*
* Note that all callbacks (onXXX methods) are all called on the main thread.
*/
interface Controller {
/**
* The container in which the view that started the animation will be animating together
* with the opening window.
*
* 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.
*
* 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
* animation.
*/
var launchContainer: ViewGroup
/**
* Return the [State] of the view that will be animated. We will animate from this state to
* the final window state.
*
* Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
* animation.
*/
fun createAnimatorState(): State
/**
* The animation started. This is typically used to initialize any additional resource
* needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
* fully above the [launchContainer].
*/
fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
/** The animation made progress and the expandable view [state] should be updated. */
fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
/**
* The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
* called previously. This is typically used to clean up the resources initialized when the
* animation was started.
*/
fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
}
/** The state of an expandable view during a [LaunchAnimator] animation. */
open class State(
/** The position of the view in screen space coordinates. */
var top: Int = 0,
var bottom: Int = 0,
var left: Int = 0,
var right: Int = 0,
var topCornerRadius: Float = 0f,
var bottomCornerRadius: Float = 0f
) {
private val startTop = top
val width: Int
get() = right - left
val height: Int
get() = bottom - top
open val topChange: Int
get() = top - startTop
val centerX: Float
get() = left + width / 2f
val centerY: Float
get() = top + height / 2f
/** Whether the expanding view should be visible or hidden. */
var visible: Boolean = true
}
interface Animation {
/** Cancel the animation. */
fun cancel()
}
/**
* Start a launch animation controlled by [controller] towards [endState]. An intermediary
* layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and
* should be the same background color as the opening (or closing) window. If [drawHole] is
* true, then this intermediary layer will be drawn with SRC blending mode while it fades out.
*
* TODO(b/184121838): Remove [drawHole] and instead make the StatusBar draw this hole instead.
*/
fun startAnimation(
controller: Controller,
endState: State,
windowBackgroundColor: Int,
drawHole: Boolean = false
): Animation {
val state = controller.createAnimatorState()
// Start state.
val startTop = state.top
val startBottom = state.bottom
val startLeft = state.left
val startRight = state.right
val startCenterX = (startLeft + startRight) / 2f
val startWidth = startRight - startLeft
val startTopCornerRadius = state.topCornerRadius
val startBottomCornerRadius = state.bottomCornerRadius
// End state.
var endTop = endState.top
var endBottom = endState.bottom
var endLeft = endState.left
var endRight = endState.right
var endCenterX = (endLeft + endRight) / 2f
var endWidth = endRight - endLeft
val endTopCornerRadius = endState.topCornerRadius
val endBottomCornerRadius = endState.bottomCornerRadius
fun maybeUpdateEndState() {
if (endTop != endState.top || endBottom != endState.bottom ||
endLeft != endState.left || endRight != endState.right) {
endTop = endState.top
endBottom = endState.bottom
endLeft = endState.left
endRight = endState.right
endCenterX = (endLeft + endRight) / 2f
endWidth = endRight - endLeft
}
}
val launchContainer = controller.launchContainer
val isExpandingFullyAbove = isExpandingFullyAbove(launchContainer, endState)
// We add an extra layer with the same color as the dialog/app splash screen background
// color, which is usually the same color of the app background. We first fade in this layer
// to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
// launch container and reveal the opening window.
val windowBackgroundLayer = GradientDrawable().apply {
setColor(windowBackgroundColor)
alpha = 0
}
// Update state.
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.duration = if (isForTesting) 0 else ANIMATION_DURATION
animator.interpolator = Interpolators.LINEAR
val launchContainerOverlay = launchContainer.overlay
var cancelled = false
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
if (DEBUG) {
Log.d(TAG, "Animation started")
}
controller.onLaunchAnimationStart(isExpandingFullyAbove)
// Add the drawable to the launch container overlay. Overlays always draw
// drawables after views, so we know that it will be drawn above any view added
// by the controller.
launchContainerOverlay.add(windowBackgroundLayer)
}
override fun onAnimationEnd(animation: Animator?) {
if (DEBUG) {
Log.d(TAG, "Animation ended")
}
controller.onLaunchAnimationEnd(isExpandingFullyAbove)
launchContainerOverlay.remove(windowBackgroundLayer)
}
})
animator.addUpdateListener { animation ->
if (cancelled) {
// TODO(b/184121838): Cancel the animator directly instead of just skipping the
// update.
return@addUpdateListener
}
maybeUpdateEndState()
// TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non
// reversed animation.
val linearProgress = animation.animatedFraction
val progress = animationInterpolator.getInterpolation(linearProgress)
val xProgress = animationInterpolatorX.getInterpolation(linearProgress)
val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress)
val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f
state.top = MathUtils.lerp(startTop, endTop, progress).roundToInt()
state.bottom = MathUtils.lerp(startBottom, endBottom, progress).roundToInt()
state.left = (xCenter - halfWidth).roundToInt()
state.right = (xCenter + halfWidth).roundToInt()
state.topCornerRadius =
MathUtils.lerp(startTopCornerRadius, endTopCornerRadius, progress)
state.bottomCornerRadius =
MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress)
// The expanding view can/should be hidden once it is completely covered by the opening
// window.
state.visible = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1
applyStateToWindowBackgroundLayer(
windowBackgroundLayer,
state,
linearProgress,
launchContainer,
drawHole
)
controller.onLaunchAnimationProgress(state, progress, linearProgress)
}
animator.start()
return object : Animation {
override fun cancel() {
cancelled = true
animator.cancel()
}
}
}
/** Return whether we are expanding fully above the [launchContainer]. */
internal fun isExpandingFullyAbove(launchContainer: View, endState: State): Boolean {
launchContainer.getLocationOnScreen(launchContainerLocation)
return endState.top <= launchContainerLocation[1] &&
endState.bottom >= launchContainerLocation[1] + launchContainer.height &&
endState.left <= launchContainerLocation[0] &&
endState.right >= launchContainerLocation[0] + launchContainer.width
}
private fun applyStateToWindowBackgroundLayer(
drawable: GradientDrawable,
state: State,
linearProgress: Float,
launchContainer: View,
drawHole: Boolean
) {
// Update position.
launchContainer.getLocationOnScreen(launchContainerLocation)
drawable.setBounds(
state.left - launchContainerLocation[0],
state.top - launchContainerLocation[1],
state.right - launchContainerLocation[0],
state.bottom - launchContainerLocation[1]
)
// Update radius.
cornerRadii[0] = state.topCornerRadius
cornerRadii[1] = state.topCornerRadius
cornerRadii[2] = state.topCornerRadius
cornerRadii[3] = state.topCornerRadius
cornerRadii[4] = state.bottomCornerRadius
cornerRadii[5] = state.bottomCornerRadius
cornerRadii[6] = state.bottomCornerRadius
cornerRadii[7] = state.bottomCornerRadius
drawable.cornerRadii = cornerRadii
// We first fade in the background layer to hide the expanding view, then fade it out
// with SRC mode to draw a hole punch in the status bar and reveal the opening window.
val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT)
if (fadeInProgress < 1) {
val alpha = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(fadeInProgress)
drawable.alpha = (alpha * 0xFF).roundToInt()
} else {
val fadeOutProgress = getProgress(
linearProgress, ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress)
drawable.alpha = (alpha * 0xFF).roundToInt()
if (drawHole) {
drawable.setXfermode(SRC_MODE)
}
}
}
}

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 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.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android">
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="?android:attr/colorBackground" />
</shape>
</inset>

View File

@@ -19,7 +19,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/internet_connectivity_dialog" android:id="@+id/internet_connectivity_dialog"
android:layout_width="@dimen/internet_dialog_list_max_width" android:layout_width="@dimen/large_dialog_width"
android:layout_height="@dimen/internet_dialog_list_max_height" android:layout_height="@dimen/internet_dialog_list_max_height"
android:background="@drawable/internet_dialog_rounded_top_corner_background" android:background="@drawable/internet_dialog_rounded_top_corner_background"
android:orientation="vertical"> android:orientation="vertical">

View File

@@ -18,7 +18,7 @@
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/media_output_dialog" android:id="@+id/media_output_dialog"
android:layout_width="match_parent" android:layout_width="@dimen/large_dialog_width"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">

View File

@@ -134,6 +134,7 @@
android:background="@drawable/qs_media_light_source" android:background="@drawable/qs_media_light_source"
android:forceHasOverlappingRendering="false"> android:forceHasOverlappingRendering="false">
<LinearLayout <LinearLayout
android:id="@+id/media_seamless_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/qs_seamless_height" android:minHeight="@dimen/qs_seamless_height"

View File

@@ -104,6 +104,6 @@
<!-- When split shade is used, this panel should be aligned to the top --> <!-- When split shade is used, this panel should be aligned to the top -->
<dimen name="qs_detail_margin_top">0dp</dimen> <dimen name="qs_detail_margin_top">0dp</dimen>
<!-- Internet panel related dimensions --> <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
<dimen name="internet_dialog_list_max_width">624dp</dimen> <dimen name="large_dialog_width">624dp</dimen>
</resources> </resources>

View File

@@ -1596,7 +1596,9 @@
<!-- Internet panel related dimensions --> <!-- Internet panel related dimensions -->
<dimen name="internet_dialog_list_margin">12dp</dimen> <dimen name="internet_dialog_list_margin">12dp</dimen>
<dimen name="internet_dialog_list_max_height">646dp</dimen> <dimen name="internet_dialog_list_max_height">646dp</dimen>
<dimen name="internet_dialog_list_max_width">@dimen/match_parent</dimen>
<!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
<dimen name="large_dialog_width">@dimen/match_parent</dimen>
<!-- Signal icon in internet dialog --> <!-- Signal icon in internet dialog -->
<dimen name="signal_strength_icon_size">24dp</dimen> <dimen name="signal_strength_icon_size">24dp</dimen>

View File

@@ -331,9 +331,6 @@
<style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast"> <style name="Animation.ShutdownUi" parent="@android:style/Animation.Toast">
</style> </style>
<style name="Animation.MediaOutputDialog" parent="@android:style/Animation.InputMethod">
</style>
<!-- Standard animations for hiding and showing the status bar. --> <!-- Standard animations for hiding and showing the status bar. -->
<style name="Animation.StatusBar"> <style name="Animation.StatusBar">
</style> </style>
@@ -436,10 +433,6 @@
<item name="android:windowCloseOnTouchOutside">true</item> <item name="android:windowCloseOnTouchOutside">true</item>
</style> </style>
<style name="Theme.SystemUI.Dialog.MediaOutput">
<item name="android:windowBackground">@drawable/media_output_dialog_background</item>
</style>
<style name="QSBorderlessButton"> <style name="QSBorderlessButton">
<item name="android:padding">12dp</item> <item name="android:padding">12dp</item>
<item name="android:background">@drawable/qs_btn_borderless_rect</item> <item name="android:background">@drawable/qs_btn_borderless_rect</item>

View File

@@ -364,9 +364,9 @@ public class MediaControlPanel {
seamlessView.setVisibility(View.VISIBLE); seamlessView.setVisibility(View.VISIBLE);
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
seamlessView.setOnClickListener(v -> { seamlessView.setOnClickListener(
mMediaOutputDialogFactory.create(data.getPackageName(), true); v -> mMediaOutputDialogFactory.create(data.getPackageName(), true,
}); mPlayerViewHolder.getSeamlessButton()));
ImageView iconView = mPlayerViewHolder.getSeamlessIcon(); ImageView iconView = mPlayerViewHolder.getSeamlessIcon();
TextView deviceName = mPlayerViewHolder.getSeamlessText(); TextView deviceName = mPlayerViewHolder.getSeamlessText();

View File

@@ -43,6 +43,7 @@ class PlayerViewHolder private constructor(itemView: View) {
val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless) val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless)
val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image) val seamlessIcon = itemView.requireViewById<ImageView>(R.id.media_seamless_image)
val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text) val seamlessText = itemView.requireViewById<TextView>(R.id.media_seamless_text)
val seamlessButton = itemView.requireViewById<View>(R.id.media_seamless_button)
// Seek bar // Seek bar
val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar) val seekBar = itemView.requireViewById<SeekBar>(R.id.media_progress_bar)

View File

@@ -45,11 +45,14 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
private static final String TAG = "MediaOutputAdapter"; private static final String TAG = "MediaOutputAdapter";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final MediaOutputDialog mMediaOutputDialog;
private ViewGroup mConnectedItem; private ViewGroup mConnectedItem;
private boolean mIncludeDynamicGroup; private boolean mIncludeDynamicGroup;
public MediaOutputAdapter(MediaOutputController controller) { public MediaOutputAdapter(MediaOutputController controller,
MediaOutputDialog mediaOutputDialog) {
super(controller); super(controller);
mMediaOutputDialog = mediaOutputDialog;
} }
@Override @Override
@@ -136,7 +139,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
mDivider.setTransitionAlpha(1); mDivider.setTransitionAlpha(1);
mAddIcon.setVisibility(View.VISIBLE); mAddIcon.setVisibility(View.VISIBLE);
mAddIcon.setTransitionAlpha(1); mAddIcon.setTransitionAlpha(1);
mAddIcon.setOnClickListener(v -> onEndItemClick()); mAddIcon.setOnClickListener(this::onEndItemClick);
} else { } else {
// Init non-active device layout // Init non-active device layout
mDivider.setVisibility(View.GONE); mDivider.setVisibility(View.GONE);
@@ -197,7 +200,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
mDivider.setTransitionAlpha(1); mDivider.setTransitionAlpha(1);
mAddIcon.setVisibility(View.VISIBLE); mAddIcon.setVisibility(View.VISIBLE);
mAddIcon.setTransitionAlpha(1); mAddIcon.setTransitionAlpha(1);
mAddIcon.setOnClickListener(v -> onEndItemClick()); mAddIcon.setOnClickListener(this::onEndItemClick);
} else { } else {
mDivider.setVisibility(View.GONE); mDivider.setVisibility(View.GONE);
mAddIcon.setVisibility(View.GONE); mAddIcon.setVisibility(View.GONE);
@@ -232,8 +235,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
} }
} }
private void onEndItemClick() { private void onEndItemClick(View view) {
mController.launchMediaOutputGroupDialog(); mController.launchMediaOutputGroupDialog(mMediaOutputDialog.getDialogView());
} }
} }
} }

View File

@@ -82,7 +82,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
}; };
public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) { public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
super(context, R.style.Theme_SystemUI_Dialog_MediaOutput); super(context);
mContext = context; mContext = context;
mMediaOutputController = mediaOutputController; mMediaOutputController = mediaOutputController;
mLayoutManager = new LinearLayoutManager(mContext); mLayoutManager = new LinearLayoutManager(mContext);
@@ -97,15 +97,15 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null); mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null);
final Window window = getWindow(); final Window window = getWindow();
final WindowManager.LayoutParams lp = window.getAttributes(); final WindowManager.LayoutParams lp = window.getAttributes();
lp.gravity = Gravity.BOTTOM; lp.gravity = Gravity.CENTER;
// Config insets to make sure the layout is above the navigation bar // Config insets to make sure the layout is above the navigation bar
lp.setFitInsetsTypes(statusBars() | navigationBars()); lp.setFitInsetsTypes(statusBars() | navigationBars());
lp.setFitInsetsSides(WindowInsets.Side.all()); lp.setFitInsetsSides(WindowInsets.Side.all());
lp.setFitInsetsIgnoringVisibility(true); lp.setFitInsetsIgnoringVisibility(true);
window.setAttributes(lp); window.setAttributes(lp);
window.setContentView(mDialogView); window.setContentView(mDialogView);
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); window.setLayout(mContext.getResources().getDimensionPixelSize(R.dimen.large_dialog_width),
window.setWindowAnimations(R.style.Animation_MediaOutputDialog); ViewGroup.LayoutParams.WRAP_CONTENT);
mHeaderTitle = mDialogView.requireViewById(R.id.header_title); mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle); mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
@@ -229,4 +229,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
void onHeaderIconClick() { void onHeaderIconClick() {
} }
View getDialogView() {
return mDialogView;
}
} }

View File

@@ -32,6 +32,7 @@ import android.os.UserHandle;
import android.os.UserManager; import android.os.UserManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@@ -48,6 +49,7 @@ import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.media.MediaOutputConstants; import com.android.settingslib.media.MediaOutputConstants;
import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -73,6 +75,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
private final MediaSessionManager mMediaSessionManager; private final MediaSessionManager mMediaSessionManager;
private final ShadeController mShadeController; private final ShadeController mShadeController;
private final ActivityStarter mActivityStarter; private final ActivityStarter mActivityStarter;
private final DialogLaunchAnimator mDialogLaunchAnimator;
private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
private final boolean mAboveStatusbar; private final boolean mAboveStatusbar;
private final NotificationEntryManager mNotificationEntryManager; private final NotificationEntryManager mNotificationEntryManager;
@@ -82,6 +85,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
private MediaController mMediaController; private MediaController mMediaController;
@VisibleForTesting @VisibleForTesting
Callback mCallback; Callback mCallback;
Callback mPreviousCallback;
@VisibleForTesting @VisibleForTesting
LocalMediaManager mLocalMediaManager; LocalMediaManager mLocalMediaManager;
@@ -92,7 +96,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
public MediaOutputController(@NonNull Context context, String packageName, public MediaOutputController(@NonNull Context context, String packageName,
boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager
lbm, ShadeController shadeController, ActivityStarter starter, lbm, ShadeController shadeController, ActivityStarter starter,
NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger) { NotificationEntryManager notificationEntryManager, UiEventLogger uiEventLogger,
DialogLaunchAnimator dialogLaunchAnimator) {
mContext = context; mContext = context;
mPackageName = packageName; mPackageName = packageName;
mMediaSessionManager = mediaSessionManager; mMediaSessionManager = mediaSessionManager;
@@ -104,6 +109,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
mUiEventLogger = uiEventLogger; mUiEventLogger = uiEventLogger;
mDialogLaunchAnimator = dialogLaunchAnimator;
} }
void start(@NonNull Callback cb) { void start(@NonNull Callback cb) {
@@ -129,7 +135,19 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
} }
return; return;
} }
if (mPreviousCallback != null) {
Log.w(TAG,
"Callback started when mPreviousCallback is not null, which is unexpected");
mPreviousCallback.dismissDialog();
}
// If we start the output group dialog when the output dialog is shown, we need to keep a
// reference to the output dialog to set it back as the callback once we dismiss the output
// group dialog.
mPreviousCallback = mCallback;
mCallback = cb; mCallback = cb;
mLocalMediaManager.unregisterCallback(this); mLocalMediaManager.unregisterCallback(this);
mLocalMediaManager.stopScan(); mLocalMediaManager.stopScan();
mLocalMediaManager.registerCallback(this); mLocalMediaManager.registerCallback(this);
@@ -145,6 +163,15 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
mLocalMediaManager.stopScan(); mLocalMediaManager.stopScan();
} }
mMediaDevices.clear(); mMediaDevices.clear();
// If there was a previous callback, i.e. we just dismissed the output group dialog and are
// now back on the output dialog, then we reset the callback to its previous value.
mCallback = null;
Callback previous = mPreviousCallback;
mPreviousCallback = null;
if (previous != null) {
start(previous);
}
} }
@Override @Override
@@ -436,6 +463,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
} }
void launchBluetoothPairing() { 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();
mCallback.dismissDialog(); mCallback.dismissDialog();
final ActivityStarter.OnDismissAction postKeyguardAction = () -> { final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
mContext.sendBroadcast(new Intent() mContext.sendBroadcast(new Intent()
@@ -447,14 +478,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true); mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
} }
void launchMediaOutputDialog() { void launchMediaOutputGroupDialog(View mediaOutputDialog) {
mCallback.dismissDialog(); // We show the output group dialog from the output dialog.
new MediaOutputDialog(mContext, mAboveStatusbar, this, mUiEventLogger); MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
} mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
void launchMediaOutputGroupDialog() {
mCallback.dismissDialog();
new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
} }
boolean isActiveRemoteDevice(@NonNull MediaDevice device) { boolean isActiveRemoteDevice(@NonNull MediaDevice device) {

View File

@@ -40,11 +40,10 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
mediaOutputController, UiEventLogger uiEventLogger) { mediaOutputController, UiEventLogger uiEventLogger) {
super(context, mediaOutputController); super(context, mediaOutputController);
mUiEventLogger = uiEventLogger; mUiEventLogger = uiEventLogger;
mAdapter = new MediaOutputAdapter(mMediaOutputController); mAdapter = new MediaOutputAdapter(mMediaOutputController, this);
if (!aboveStatusbar) { if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
} }
show();
} }
@Override @Override

View File

@@ -18,8 +18,10 @@ package com.android.systemui.media.dialog
import android.content.Context import android.content.Context
import android.media.session.MediaSessionManager import android.media.session.MediaSessionManager
import android.view.View
import com.android.internal.logging.UiEventLogger import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.notification.NotificationEntryManager import com.android.systemui.statusbar.notification.NotificationEntryManager
import com.android.systemui.statusbar.phone.ShadeController import com.android.systemui.statusbar.phone.ShadeController
@@ -35,19 +37,29 @@ class MediaOutputDialogFactory @Inject constructor(
private val shadeController: ShadeController, private val shadeController: ShadeController,
private val starter: ActivityStarter, private val starter: ActivityStarter,
private val notificationEntryManager: NotificationEntryManager, private val notificationEntryManager: NotificationEntryManager,
private val uiEventLogger: UiEventLogger private val uiEventLogger: UiEventLogger,
private val dialogLaunchAnimator: DialogLaunchAnimator
) { ) {
companion object { companion object {
var mediaOutputDialog: MediaOutputDialog? = null var mediaOutputDialog: MediaOutputDialog? = null
} }
/** Creates a [MediaOutputDialog] for the given package. */ /** Creates a [MediaOutputDialog] for the given package. */
fun create(packageName: String, aboveStatusBar: Boolean) { fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
// Dismiss the previous dialog, if any.
mediaOutputDialog?.dismiss() mediaOutputDialog?.dismiss()
mediaOutputDialog = MediaOutputController(context, packageName, aboveStatusBar,
mediaSessionManager, lbm, shadeController, starter, notificationEntryManager, val controller = MediaOutputController(context, packageName, aboveStatusBar,
uiEventLogger).run { mediaSessionManager, lbm, shadeController, starter, notificationEntryManager,
MediaOutputDialog(context, aboveStatusBar, this, uiEventLogger) uiEventLogger, dialogLaunchAnimator)
val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger)
mediaOutputDialog = dialog
// Show the dialog.
if (view != null) {
dialogLaunchAnimator.showFromView(dialog, view)
} else {
dialog.show()
} }
} }

View File

@@ -38,7 +38,6 @@ public class MediaOutputGroupDialog extends MediaOutputBaseDialog {
if (!aboveStatusbar) { if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
} }
show();
} }
@Override @Override
@@ -83,6 +82,8 @@ public class MediaOutputGroupDialog extends MediaOutputBaseDialog {
@Override @Override
void onHeaderIconClick() { void onHeaderIconClick() {
mMediaOutputController.launchMediaOutputDialog(); // Given that we launched the media output group dialog from the media output dialog,
// dismissing this dialog will show the media output dialog again.
dismiss();
} }
} }

View File

@@ -206,7 +206,7 @@ public class InternetDialog extends SystemUIDialog implements
window.setContentView(mDialogView); window.setContentView(mDialogView);
//Only fix the width for large screen or tablet. //Only fix the width for large screen or tablet.
window.setLayout(mContext.getResources().getDimensionPixelSize( window.setLayout(mContext.getResources().getDimensionPixelSize(
R.dimen.internet_dialog_list_max_width), ViewGroup.LayoutParams.WRAP_CONTENT); R.dimen.large_dialog_width), ViewGroup.LayoutParams.WRAP_CONTENT);
window.setWindowAnimations(R.style.Animation_InternetDialog); window.setWindowAnimations(R.style.Animation_InternetDialog);
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
window.addFlags(FLAG_LAYOUT_NO_LIMITS); window.addFlags(FLAG_LAYOUT_NO_LIMITS);

View File

@@ -22,6 +22,9 @@ import android.content.Context;
import android.os.Handler; import android.os.Handler;
import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.animation.LaunchAnimator;
import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpManager;
@@ -62,6 +65,7 @@ import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
import com.android.systemui.statusbar.phone.SystemUIHostDialogProvider;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger;
import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.statusbar.policy.RemoteInputUriController;
@@ -261,4 +265,29 @@ public interface StatusBarDependenciesModule {
@Binds @Binds
QSCarrierGroupController.SlotIndexResolver provideSlotIndexResolver( QSCarrierGroupController.SlotIndexResolver provideSlotIndexResolver(
QSCarrierGroupController.SubscriptionManagerSlotIndexResolver impl); QSCarrierGroupController.SubscriptionManagerSlotIndexResolver impl);
/**
*/
@Provides
@SysUISingleton
static LaunchAnimator provideLaunchAnimator(Context context) {
return new LaunchAnimator(context);
}
/**
*/
@Provides
@SysUISingleton
static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) {
return new ActivityLaunchAnimator(launchAnimator);
}
/**
*/
@Provides
@SysUISingleton
static DialogLaunchAnimator provideDialogLaunchAnimator(Context context,
LaunchAnimator launchAnimator) {
return new DialogLaunchAnimator(context, launchAnimator, new SystemUIHostDialogProvider());
}
} }

View File

@@ -2,8 +2,8 @@ package com.android.systemui.statusbar.notification
import android.util.MathUtils import android.util.MathUtils
import com.android.internal.annotations.VisibleForTesting import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.Interpolators import com.android.systemui.animation.Interpolators
import com.android.systemui.animation.LaunchAnimator
import kotlin.math.min import kotlin.math.min
/** Parameters for the notifications expand animations. */ /** Parameters for the notifications expand animations. */
@@ -15,7 +15,7 @@ class ExpandAnimationParameters(
topCornerRadius: Float = 0f, topCornerRadius: Float = 0f,
bottomCornerRadius: Float = 0f bottomCornerRadius: Float = 0f
) : ActivityLaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) { ) : LaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
@VisibleForTesting @VisibleForTesting
constructor() : this( constructor() : this(
top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f
@@ -55,6 +55,6 @@ class ExpandAnimationParameters(
} }
fun getProgress(delay: Long, duration: Long): Float { fun getProgress(delay: Long, duration: Long): Float {
return ActivityLaunchAnimator.getProgress(linearProgress, delay, duration) return LaunchAnimator.getProgress(linearProgress, delay, duration)
} }
} }

View File

@@ -3,6 +3,7 @@ package com.android.systemui.statusbar.notification
import android.view.ViewGroup import android.view.ViewGroup
import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.LaunchAnimator
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
@@ -54,7 +55,7 @@ class NotificationLaunchAnimatorController(
// Do nothing. Notifications are always animated inside their rootView. // Do nothing. Notifications are always animated inside their rootView.
} }
override fun createAnimatorState(): ActivityLaunchAnimator.State { override fun createAnimatorState(): LaunchAnimator.State {
// If the notification panel is collapsed, the clip may be larger than the height. // If the notification panel is collapsed, the clip may be larger than the height.
val height = max(0, notification.actualHeight - notification.clipBottomAmount) val height = max(0, notification.actualHeight - notification.clipBottomAmount)
val location = notification.locationOnScreen val location = notification.locationOnScreen
@@ -72,12 +73,12 @@ class NotificationLaunchAnimatorController(
notification.currentBackgroundRadiusTop notification.currentBackgroundRadiusTop
} }
val params = ExpandAnimationParameters( val params = ExpandAnimationParameters(
top = windowTop, top = windowTop,
bottom = location[1] + height, bottom = location[1] + height,
left = location[0], left = location[0],
right = location[0] + notification.width, right = location[0] + notification.width,
topCornerRadius = topCornerRadius, topCornerRadius = topCornerRadius,
bottomCornerRadius = notification.currentBackgroundRadiusBottom bottomCornerRadius = notification.currentBackgroundRadiusBottom
) )
params.startTranslationZ = notification.translationZ params.startTranslationZ = notification.translationZ
@@ -86,8 +87,8 @@ class NotificationLaunchAnimatorController(
params.startClipTopAmount = notification.clipTopAmount params.startClipTopAmount = notification.clipTopAmount
if (notification.isChildInGroup) { if (notification.isChildInGroup) {
params.startNotificationTop += notification.notificationParent.translationY params.startNotificationTop += notification.notificationParent.translationY
val parentRoundedClip = Math.max(clipStartLocation val parentRoundedClip = Math.max(
- notification.notificationParent.locationOnScreen[1], 0) clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
params.parentStartRoundedTopClipping = parentRoundedClip params.parentStartRoundedTopClipping = parentRoundedClip
val parentClip = notification.notificationParent.clipTopAmount val parentClip = notification.notificationParent.clipTopAmount
@@ -157,7 +158,7 @@ class NotificationLaunchAnimatorController(
} }
override fun onLaunchAnimationProgress( override fun onLaunchAnimationProgress(
state: ActivityLaunchAnimator.State, state: LaunchAnimator.State,
progress: Float, progress: Float,
linearProgress: Float linearProgress: Float
) { ) {

View File

@@ -103,8 +103,8 @@ import com.android.keyguard.dagger.KeyguardUserSwitcherComponent;
import com.android.systemui.DejankUtils; import com.android.systemui.DejankUtils;
import com.android.systemui.Dependency; import com.android.systemui.Dependency;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.Interpolators;
import com.android.systemui.animation.LaunchAnimator;
import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.AuthController;
import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.Classifier;
import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingCollector;
@@ -212,7 +212,7 @@ public class NotificationPanelViewController extends PanelViewController {
*/ */
private static final int FLING_HIDE = 2; private static final int FLING_HIDE = 2;
private static final long ANIMATION_DELAY_ICON_FADE_IN = private static final long ANIMATION_DELAY_ICON_FADE_IN =
ActivityLaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION LaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION
- CollapsedStatusBarFragment.FADE_IN_DELAY - 48; - CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
private final DozeParameters mDozeParameters; private final DozeParameters mDozeParameters;
@@ -3594,7 +3594,7 @@ public class NotificationPanelViewController extends PanelViewController {
} }
public void applyLaunchAnimationProgress(float linearProgress) { public void applyLaunchAnimationProgress(float linearProgress) {
boolean hideIcons = ActivityLaunchAnimator.getProgress(linearProgress, boolean hideIcons = LaunchAnimator.getProgress(linearProgress,
ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f; ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
if (hideIcons != mHideIconsDuringLaunchAnimation) { if (hideIcons != mHideIconsDuringLaunchAnimation) {
mHideIconsDuringLaunchAnimation = hideIcons; mHideIconsDuringLaunchAnimation = hideIcons;

View File

@@ -135,6 +135,7 @@ import com.android.systemui.R;
import com.android.systemui.SystemUI; import com.android.systemui.SystemUI;
import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DelegateLaunchAnimatorController; import com.android.systemui.animation.DelegateLaunchAnimatorController;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.assist.AssistManager; import com.android.systemui.assist.AssistManager;
import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.battery.BatteryMeterViewController;
import com.android.systemui.biometrics.AuthRippleController; import com.android.systemui.biometrics.AuthRippleController;
@@ -669,7 +670,8 @@ public class StatusBar extends SystemUI implements
private final SysuiStatusBarStateController mStatusBarStateController; private final SysuiStatusBarStateController mStatusBarStateController;
private HeadsUpAppearanceController mHeadsUpAppearanceController; private HeadsUpAppearanceController mHeadsUpAppearanceController;
private ActivityLaunchAnimator mActivityLaunchAnimator; private final ActivityLaunchAnimator mActivityLaunchAnimator;
private final DialogLaunchAnimator mDialogLaunchAnimator;
private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider; private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
protected StatusBarNotificationPresenter mPresenter; protected StatusBarNotificationPresenter mPresenter;
private NotificationActivityStarter mNotificationActivityStarter; private NotificationActivityStarter mNotificationActivityStarter;
@@ -792,7 +794,9 @@ public class StatusBar extends SystemUI implements
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
Optional<StartingSurface> startingSurfaceOptional, Optional<StartingSurface> startingSurfaceOptional,
TunerService tunerService, TunerService tunerService,
DumpManager dumpManager) { DumpManager dumpManager,
ActivityLaunchAnimator activityLaunchAnimator,
DialogLaunchAnimator dialogLaunchAnimator) {
super(context); super(context);
mNotificationsController = notificationsController; mNotificationsController = notificationsController;
mLightBarController = lightBarController; mLightBarController = lightBarController;
@@ -900,6 +904,8 @@ public class StatusBar extends SystemUI implements
}); });
mActivityIntentHelper = new ActivityIntentHelper(mContext); mActivityIntentHelper = new ActivityIntentHelper(mContext);
mActivityLaunchAnimator = activityLaunchAnimator;
mDialogLaunchAnimator = dialogLaunchAnimator;
// TODO(b/190746471): Find a better home for this. // TODO(b/190746471): Find a better home for this.
DateTimeView.setReceiverHandler(timeTickHandler); DateTimeView.setReceiverHandler(timeTickHandler);
@@ -1467,7 +1473,7 @@ public class StatusBar extends SystemUI implements
private void setUpPresenter() { private void setUpPresenter() {
// Set up the initial notification state. // Set up the initial notification state.
mActivityLaunchAnimator = new ActivityLaunchAnimator(mKeyguardHandler, mContext); mActivityLaunchAnimator.setCallback(mKeyguardHandler);
mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider( mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider(
mNotificationShadeWindowViewController, mNotificationShadeWindowViewController,
mStackScrollerController.getNotificationListContainer(), mStackScrollerController.getNotificationListContainer(),
@@ -2549,7 +2555,8 @@ public class StatusBar extends SystemUI implements
animationController != null && !willLaunchResolverActivity && shouldAnimateLaunch( animationController != null && !willLaunchResolverActivity && shouldAnimateLaunch(
true /* isActivityIntent */); true /* isActivityIntent */);
ActivityLaunchAnimator.Controller animController = ActivityLaunchAnimator.Controller animController =
animate ? wrapAnimationController(animationController, dismissShade) : null; animationController != null ? wrapAnimationController(animationController,
dismissShade) : null;
// If we animate, we will dismiss the shade only once the animation is done. This is taken // If we animate, we will dismiss the shade only once the animation is done. This is taken
// care of by the StatusBarLaunchAnimationController. // care of by the StatusBarLaunchAnimationController.
@@ -4426,6 +4433,8 @@ public class StatusBar extends SystemUI implements
&& !mBiometricUnlockController.isWakeAndUnlock()) { && !mBiometricUnlockController.isWakeAndUnlock()) {
mLightRevealScrim.setRevealAmount(1f - linear); mLightRevealScrim.setRevealAmount(1f - linear);
} }
mDialogLaunchAnimator.onDozeAmountChanged(linear);
} }
@Override @Override

View File

@@ -1,6 +1,7 @@
package com.android.systemui.statusbar.phone package com.android.systemui.statusbar.phone
import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.LaunchAnimator
/** /**
* A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right * A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right
@@ -22,7 +23,7 @@ class StatusBarLaunchAnimatorController(
delegate.onLaunchAnimationStart(isExpandingFullyAbove) delegate.onLaunchAnimationStart(isExpandingFullyAbove)
statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true) statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true)
if (!isExpandingFullyAbove) { if (!isExpandingFullyAbove) {
statusBar.collapsePanelWithDuration(ActivityLaunchAnimator.ANIMATION_DURATION.toInt()) statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt())
} }
} }
@@ -33,7 +34,7 @@ class StatusBarLaunchAnimatorController(
} }
override fun onLaunchAnimationProgress( override fun onLaunchAnimationProgress(
state: ActivityLaunchAnimator.State, state: LaunchAnimator.State,
progress: Float, progress: Float,
linearProgress: Float linearProgress: Float
) { ) {

View File

@@ -30,19 +30,24 @@ import android.view.WindowManager.LayoutParams;
import com.android.systemui.Dependency; import com.android.systemui.Dependency;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.animation.DialogListener;
import com.android.systemui.animation.ListenableDialog;
import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.KeyguardStateController;
import java.util.LinkedHashSet;
import java.util.Set;
/** /**
* Base class for dialogs that should appear over panels and keyguard. * Base class for dialogs that should appear over panels and keyguard.
* The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast, * The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast,
* and dismisses itself when it receives the broadcast. * and dismisses itself when it receives the broadcast.
*/ */
public class SystemUIDialog extends AlertDialog { public class SystemUIDialog extends AlertDialog implements ListenableDialog {
private final Context mContext; private final Context mContext;
private final DismissReceiver mDismissReceiver; private final DismissReceiver mDismissReceiver;
private final Set<DialogListener> mDialogListeners = new LinkedHashSet<>();
public SystemUIDialog(Context context) { public SystemUIDialog(Context context) {
this(context, R.style.Theme_SystemUI_Dialog); this(context, R.style.Theme_SystemUI_Dialog);
@@ -72,6 +77,43 @@ public class SystemUIDialog extends AlertDialog {
mDismissReceiver.unregister(); mDismissReceiver.unregister();
} }
@Override
public void addListener(DialogListener listener) {
mDialogListeners.add(listener);
}
@Override
public void removeListener(DialogListener listener) {
mDialogListeners.remove(listener);
}
@Override
public void dismiss() {
super.dismiss();
for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) {
listener.onDismiss();
}
}
@Override
public void hide() {
super.hide();
for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) {
listener.onHide();
}
}
@Override
public void show() {
super.show();
for (DialogListener listener : new LinkedHashSet<>(mDialogListeners)) {
listener.onShow();
}
}
public void setShowForAllUsers(boolean show) { public void setShowForAllUsers(boolean show) {
setShowForAllUsers(this, show); setShowForAllUsers(this, show);
} }

View File

@@ -0,0 +1,36 @@
package com.android.systemui.statusbar.phone
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import com.android.systemui.animation.HostDialogProvider
/** An implementation of [HostDialogProvider] to be used when animating SysUI dialogs. */
class SystemUIHostDialogProvider : HostDialogProvider {
override fun createHostDialog(
context: Context,
theme: Int,
onCreateCallback: () -> Unit,
dismissOverride: (() -> Unit) -> Unit
): Dialog {
return SystemUIHostDialog(context, theme, onCreateCallback, dismissOverride)
}
private class SystemUIHostDialog(
context: Context,
theme: Int,
private val onCreateCallback: () -> Unit,
private val dismissOverride: (() -> Unit) -> Unit
) : SystemUIDialog(context, theme) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onCreateCallback()
}
override fun dismiss() {
dismissOverride {
super.dismiss()
}
}
}
}

View File

@@ -28,6 +28,8 @@ import com.android.internal.logging.MetricsLogger;
import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.ViewMediatorCallback; import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.InitController; import com.android.systemui.InitController;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.assist.AssistManager; import com.android.systemui.assist.AssistManager;
import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingCollector;
@@ -235,7 +237,9 @@ public interface StatusBarPhoneModule {
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
Optional<StartingSurface> startingSurfaceOptional, Optional<StartingSurface> startingSurfaceOptional,
TunerService tunerService, TunerService tunerService,
DumpManager dumpManager) { DumpManager dumpManager,
ActivityLaunchAnimator activityLaunchAnimator,
DialogLaunchAnimator dialogLaunchAnimator) {
return new StatusBar( return new StatusBar(
context, context,
notificationsController, notificationsController,
@@ -333,6 +337,8 @@ public interface StatusBarPhoneModule {
unlockedScreenOffAnimationController, unlockedScreenOffAnimationController,
startingSurfaceOptional, startingSurfaceOptional,
tunerService, tunerService,
dumpManager); dumpManager,
activityLaunchAnimator,
dialogLaunchAnimator);
} }
} }

View File

@@ -46,6 +46,7 @@ import org.mockito.junit.MockitoJUnit
@RunWithLooper @RunWithLooper
class ActivityLaunchAnimatorTest : SysuiTestCase() { class ActivityLaunchAnimatorTest : SysuiTestCase() {
private val launchContainer = LinearLayout(mContext) private val launchContainer = LinearLayout(mContext)
private val launchAnimator = LaunchAnimator(mContext, isForTesting = true)
@Mock lateinit var callback: ActivityLaunchAnimator.Callback @Mock lateinit var callback: ActivityLaunchAnimator.Callback
@Spy private val controller = TestLaunchAnimatorController(launchContainer) @Spy private val controller = TestLaunchAnimatorController(launchContainer)
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
@@ -56,7 +57,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
@Before @Before
fun setup() { fun setup() {
activityLaunchAnimator = ActivityLaunchAnimator(callback, mContext) activityLaunchAnimator = ActivityLaunchAnimator(launchAnimator)
activityLaunchAnimator.callback = callback
} }
private fun startIntentWithAnimation( private fun startIntentWithAnimation(
@@ -120,7 +122,8 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
@Test @Test
fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() { fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
`when`(callback.isOnKeyguard()).thenReturn(true) `when`(callback.isOnKeyguard()).thenReturn(true)
val animator = ActivityLaunchAnimator(callback, context) val animator = ActivityLaunchAnimator(launchAnimator)
animator.callback = callback
val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
var animationAdapter: RemoteAnimationAdapter? = null var animationAdapter: RemoteAnimationAdapter? = null
@@ -208,7 +211,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
private class TestLaunchAnimatorController( private class TestLaunchAnimatorController(
override var launchContainer: ViewGroup override var launchContainer: ViewGroup
) : ActivityLaunchAnimator.Controller { ) : ActivityLaunchAnimator.Controller {
override fun createAnimatorState() = ActivityLaunchAnimator.State( override fun createAnimatorState() = LaunchAnimator.State(
top = 100, top = 100,
bottom = 200, bottom = 200,
left = 300, left = 300,
@@ -232,7 +235,7 @@ private class TestLaunchAnimatorController(
} }
override fun onLaunchAnimationProgress( override fun onLaunchAnimationProgress(
state: ActivityLaunchAnimator.State, state: LaunchAnimator.State,
progress: Float, progress: Float,
linearProgress: Float linearProgress: Float
) { ) {

View File

@@ -0,0 +1,186 @@
package com.android.systemui.animation
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.ViewUtils
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class DialogLaunchAnimatorTest : SysuiTestCase() {
private val launchAnimator = LaunchAnimator(context, isForTesting = true)
private val hostDialogprovider = TestHostDialogProvider()
private val dialogLaunchAnimator =
DialogLaunchAnimator(context, launchAnimator, hostDialogprovider)
@Test
fun testShowDialogFromView() {
// Show the dialog. showFromView() must be called on the main thread with a dialog created
// on the main thread too.
val (dialog, hostDialog) = runOnMainThreadAndWaitForIdleSync {
val touchSurfaceRoot = LinearLayout(context)
val touchSurface = View(context)
touchSurfaceRoot.addView(touchSurface)
// We need to attach the root to the window manager otherwise the exit animation will
// be skipped
ViewUtils.attachView(touchSurfaceRoot)
val dialog = TestDialog(context)
val hostDialog =
dialogLaunchAnimator.showFromView(dialog, touchSurface) as TestHostDialog
dialog to hostDialog
}
// Only the host dialog is actually showing.
assertTrue(hostDialog.isShowing)
assertFalse(dialog.isShowing)
// The dialog onStart() method was called but not onStop().
assertTrue(dialog.onStartCalled)
assertFalse(dialog.onStopCalled)
// The dialog content has been stolen and is shown inside the host dialog.
val hostDialogContent = hostDialog.findViewById<ViewGroup>(android.R.id.content)
assertEquals(0, dialog.findViewById<ViewGroup>(android.R.id.content).childCount)
assertEquals(1, hostDialogContent.childCount)
val hostDialogRoot = hostDialogContent.getChildAt(0) as ViewGroup
assertEquals(1, hostDialogRoot.childCount)
assertEquals(dialog.contentView, hostDialogRoot.getChildAt(0))
// If we are dozing, the host dialog window also fades out.
runOnMainThreadAndWaitForIdleSync { dialogLaunchAnimator.onDozeAmountChanged(0.5f) }
assertTrue(hostDialog.window!!.decorView.alpha < 1f)
// Hiding/showing/dismissing the dialog should hide/show/dismiss the host dialog given that
// it's a ListenableDialog.
runOnMainThreadAndWaitForIdleSync { dialog.hide() }
assertFalse(hostDialog.isShowing)
assertFalse(dialog.isShowing)
runOnMainThreadAndWaitForIdleSync { dialog.show() }
assertTrue(hostDialog.isShowing)
assertFalse(dialog.isShowing)
assertFalse(dialog.onStopCalled)
runOnMainThreadAndWaitForIdleSync { dialog.dismiss() }
assertFalse(hostDialog.isShowing)
assertFalse(dialog.isShowing)
assertTrue(hostDialog.wasDismissed)
assertTrue(dialog.onStopCalled)
}
private fun <T : Any> runOnMainThreadAndWaitForIdleSync(f: () -> T): T {
lateinit var result: T
context.mainExecutor.execute {
result = f()
}
waitForIdleSync()
return result
}
private class TestHostDialogProvider : HostDialogProvider {
override fun createHostDialog(
context: Context,
theme: Int,
onCreateCallback: () -> Unit,
dismissOverride: (() -> Unit) -> Unit
): Dialog = TestHostDialog(context, onCreateCallback, dismissOverride)
}
private class TestHostDialog(
context: Context,
private val onCreateCallback: () -> Unit,
private val dismissOverride: (() -> Unit) -> Unit
) : Dialog(context) {
var wasDismissed = false
init {
// We need to set the window type for dialogs shown by SysUI, otherwise WM will throw.
window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onCreateCallback()
}
override fun dismiss() {
dismissOverride {
super.dismiss()
wasDismissed = true
}
}
}
private class TestDialog(context: Context) : Dialog(context), ListenableDialog {
private val listeners = hashSetOf<DialogListener>()
val contentView = View(context)
var onStartCalled = false
var onStopCalled = false
init {
// We need to set the window type for dialogs shown by SysUI, otherwise WM will throw.
window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(contentView)
}
override fun onStart() {
super.onStart()
onStartCalled = true
}
override fun onStop() {
super.onStart()
onStopCalled = true
}
override fun addListener(listener: DialogListener) {
listeners.add(listener)
}
override fun removeListener(listener: DialogListener) {
listeners.remove(listener)
}
override fun dismiss() {
super.dismiss()
notifyListeners { onDismiss() }
}
override fun hide() {
super.hide()
notifyListeners { onHide() }
}
override fun show() {
super.show()
notifyListeners { onShow() }
}
private fun notifyListeners(notify: DialogListener.() -> Unit) {
for (listener in HashSet(listeners)) {
listener.notify()
}
}
}
}

View File

@@ -32,7 +32,7 @@ class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
fun animatingOrphanViewDoesNotCrash() { fun animatingOrphanViewDoesNotCrash() {
val ghostedView = LinearLayout(mContext) val ghostedView = LinearLayout(mContext)
val controller = GhostedViewLaunchAnimatorController(ghostedView) val controller = GhostedViewLaunchAnimatorController(ghostedView)
val state = ActivityLaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0) val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
controller.onIntentStarted(willAnimate = true) controller.onIntentStarted(willAnimate = true)
controller.onLaunchAnimationStart(isExpandingFullyAbove = true) controller.onLaunchAnimationStart(isExpandingFullyAbove = true)

View File

@@ -54,6 +54,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
// Mock // Mock
private MediaOutputController mMediaOutputController = mock(MediaOutputController.class); private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
private MediaOutputDialog mMediaOutputDialog = mock(MediaOutputDialog.class);
private MediaDevice mMediaDevice1 = mock(MediaDevice.class); private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
private MediaDevice mMediaDevice2 = mock(MediaDevice.class); private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
private Icon mIcon = mock(Icon.class); private Icon mIcon = mock(Icon.class);
@@ -65,7 +66,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
@Before @Before
public void setUp() { public void setUp() {
mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController); mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController, mMediaOutputDialog);
mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0); .onCreateViewHolder(new LinearLayout(mContext), 0);

View File

@@ -39,6 +39,7 @@ import com.android.internal.logging.UiEventLogger;
import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.phone.ShadeController;
@@ -63,6 +64,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
private NotificationEntryManager mNotificationEntryManager = private NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class); mock(NotificationEntryManager.class);
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl; private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
private MediaOutputController mMediaOutputController; private MediaOutputController mMediaOutputController;
@@ -75,7 +77,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
public void setUp() { public void setUp() {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger); mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
mMediaOutputController); mMediaOutputController);
mMediaOutputBaseDialogImpl.onCreate(new Bundle()); mMediaOutputBaseDialogImpl.onCreate(new Bundle());

View File

@@ -49,6 +49,7 @@ import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -91,6 +92,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
private NotificationEntryManager mNotificationEntryManager = private NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class); mock(NotificationEntryManager.class);
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
private Context mSpyContext; private Context mSpyContext;
private MediaOutputController mMediaOutputController; private MediaOutputController mMediaOutputController;
@@ -113,7 +115,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false, mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger); mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
MediaDescription.Builder builder = new MediaDescription.Builder(); MediaDescription.Builder builder = new MediaDescription.Builder();
@@ -157,7 +159,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
public void start_withoutPackageName_verifyMediaControllerInit() { public void start_withoutPackageName_verifyMediaControllerInit() {
mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger); mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputController.start(mCb); mMediaOutputController.start(mCb);
@@ -178,7 +180,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
public void stop_withoutPackageName_verifyMediaControllerDeinit() { public void stop_withoutPackageName_verifyMediaControllerDeinit() {
mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger); mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputController.start(mCb); mMediaOutputController.start(mCb);
@@ -449,7 +451,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
public void getNotificationLargeIcon_withoutPackageName_returnsNull() { public void getNotificationLargeIcon_withoutPackageName_returnsNull() {
mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaOutputController = new MediaOutputController(mSpyContext, null, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger); mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
assertThat(mMediaOutputController.getNotificationIcon()).isNull(); assertThat(mMediaOutputController.getNotificationIcon()).isNull();
} }

View File

@@ -36,6 +36,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaDevice;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.phone.ShadeController;
@@ -65,6 +66,7 @@ public class MediaOutputDialogTest extends SysuiTestCase {
private final NotificationEntryManager mNotificationEntryManager = private final NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class); mock(NotificationEntryManager.class);
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
private MediaOutputDialog mMediaOutputDialog; private MediaOutputDialog mMediaOutputDialog;
private MediaOutputController mMediaOutputController; private MediaOutputController mMediaOutputController;
@@ -74,10 +76,11 @@ public class MediaOutputDialogTest extends SysuiTestCase {
public void setUp() { public void setUp() {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger); mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
mMediaOutputDialog = new MediaOutputDialog(mContext, false, mMediaOutputDialog = new MediaOutputDialog(mContext, false,
mMediaOutputController, mUiEventLogger); mMediaOutputController, mUiEventLogger);
mMediaOutputDialog.show();
when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice); when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice);
when(mMediaDevice.getFeatures()).thenReturn(mFeatures); when(mMediaDevice.getFeatures()).thenReturn(mFeatures);
@@ -123,6 +126,7 @@ public class MediaOutputDialogTest extends SysuiTestCase {
public void onCreate_ShouldLogVisibility() { public void onCreate_ShouldLogVisibility() {
MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false, MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false,
mMediaOutputController, mUiEventLogger); mMediaOutputController, mUiEventLogger);
testDialog.show();
testDialog.dismissDialog(); testDialog.dismissDialog();

View File

@@ -34,6 +34,7 @@ import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.phone.ShadeController;
@@ -64,6 +65,7 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase {
private NotificationEntryManager mNotificationEntryManager = private NotificationEntryManager mNotificationEntryManager =
mock(NotificationEntryManager.class); mock(NotificationEntryManager.class);
private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
private MediaOutputGroupDialog mMediaOutputGroupDialog; private MediaOutputGroupDialog mMediaOutputGroupDialog;
private MediaOutputController mMediaOutputController; private MediaOutputController mMediaOutputController;
@@ -73,10 +75,11 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase {
public void setUp() { public void setUp() {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false,
mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter,
mNotificationEntryManager, mUiEventLogger); mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false, mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false,
mMediaOutputController); mMediaOutputController);
mMediaOutputGroupDialog.show();
when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices); when(mLocalMediaManager.getSelectedMediaDevice()).thenReturn(mMediaDevices);
} }

View File

@@ -77,6 +77,8 @@ import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.InitController; import com.android.systemui.InitController;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.assist.AssistManager; import com.android.systemui.assist.AssistManager;
import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingCollectorFake;
@@ -274,6 +276,8 @@ public class StatusBarTest extends SysuiTestCase {
@Mock private StartingSurface mStartingSurface; @Mock private StartingSurface mStartingSurface;
@Mock private OperatorNameViewController mOperatorNameViewController; @Mock private OperatorNameViewController mOperatorNameViewController;
@Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory; @Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
@Mock private ActivityLaunchAnimator mActivityLaunchAnimator;
@Mock private DialogLaunchAnimator mDialogLaunchAnimator;
private ShadeController mShadeController; private ShadeController mShadeController;
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock); private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
@@ -452,7 +456,9 @@ public class StatusBarTest extends SysuiTestCase {
mUnlockedScreenOffAnimationController, mUnlockedScreenOffAnimationController,
Optional.of(mStartingSurface), Optional.of(mStartingSurface),
mTunerService, mTunerService,
mock(DumpManager.class)); mock(DumpManager.class),
mActivityLaunchAnimator,
mDialogLaunchAnimator);
when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class), when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),
any(NotificationPanelViewController.class), any(BiometricUnlockController.class), any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
any(ViewGroup.class), any(KeyguardBypassController.class))) any(ViewGroup.class), any(KeyguardBypassController.class)))