Animate dialog to app launches (1/2)

See b/214961273#comment1 and #comment2 for before/after videos as well
as technical explanations on how this animation works.

This CL also enables the generation of Java 8 default method bodies in
interfaces, in the animation library. This is necessary to avoid
unexpected behaviors when using @JvmDefault on an interface +
delegation, which is a common pattern when using the animation library.
See [1] and [2] for more info. Note that this is enabled only for the
animation library (and not SystemUI(Lib)) to avoid breaking code
depending on the (now deprecated) @JvmDefault method in SysUI.

[1] https://youtrack.jetbrains.com/issue/KT-34612
[2] https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/

Bug: 214961273
Test: atest DialogLaunchAnimatorTest
Test: Manual
Change-Id: I27d53400a5dc852543a68f5b6f107d9eb08b06db
This commit is contained in:
Jordan Demeulenaere
2022-01-17 11:28:09 +01:00
parent 406e462e45
commit 6e77c58de8
21 changed files with 489 additions and 133 deletions

View File

@@ -39,5 +39,5 @@ android_library {
],
manifest: "AndroidManifest.xml",
kotlincflags: ["-Xjvm-default=enable"],
kotlincflags: ["-Xjvm-default=all"],
}

View File

@@ -48,9 +48,16 @@ private const val TAG = "ActivityLaunchAnimator"
* nicely into the starting window.
*/
class ActivityLaunchAnimator(
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS)
/** The animator used when animating a View into an app. */
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
/** The animator used when animating a Dialog into an app. */
// TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to
// TIMINGS.contentBeforeFadeOutDuration.
private val dialogToAppAnimator: LaunchAnimator = LaunchAnimator(DIALOG_TIMINGS, INTERPOLATORS)
) {
companion object {
/** The timings when animating a View into an app. */
@JvmField
val TIMINGS = LaunchAnimator.Timings(
totalDuration = 500L,
@@ -60,6 +67,17 @@ class ActivityLaunchAnimator(
contentAfterFadeInDuration = 183L
)
/**
* The timings when animating a Dialog into an app. We need to wait at least 200ms before
* showing the app (which is under the dialog window) so that the dialog window dim is fully
* faded out, to avoid flicker.
*/
val DIALOG_TIMINGS = TIMINGS.copy(
contentBeforeFadeOutDuration = 200L,
contentAfterFadeInDelay = 200L
)
/** The interpolators when animating a View or a dialog into an app. */
val INTERPOLATORS = LaunchAnimator.Interpolators(
positionInterpolator = Interpolators.EMPHASIZED,
positionXInterpolator = createPositionXInterpolator(),
@@ -297,11 +315,18 @@ class ActivityLaunchAnimator(
}
}
/**
* Whether this controller is controlling a dialog launch. This will be used to adapt the
* timings, making sure we don't show the app until the dialog dim had the time to fade out.
*/
// TODO(b/218989950): Remove this.
val isDialogLaunch: Boolean
get() = false
/**
* The intent was started. If [willAnimate] is false, nothing else will happen and the
* animation will not be started.
*/
@JvmDefault
fun onIntentStarted(willAnimate: Boolean) {}
/**
@@ -309,7 +334,6 @@ class ActivityLaunchAnimator(
* this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
* before the cancellation.
*/
@JvmDefault
fun onLaunchAnimationCancelled() {}
}
@@ -317,7 +341,9 @@ class ActivityLaunchAnimator(
inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
private val launchContainer = controller.launchContainer
private val context = launchContainer.context
private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer)
private val transactionApplierView =
controller.openingWindowSyncView ?: controller.launchContainer
private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView)
private val matrix = Matrix()
private val invertMatrix = Matrix()
@@ -405,6 +431,13 @@ class ActivityLaunchAnimator(
val callback = this@ActivityLaunchAnimator.callback!!
val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
// Make sure we use the modified timings when animating a dialog into an app.
val launchAnimator = if (controller.isDialogLaunch) {
dialogToAppAnimator
} else {
launchAnimator
}
// TODO(b/184121838): We should somehow get the top and bottom radius of the window
// instead of recomputing isExpandingFullyAbove here.
val isExpandingFullyAbove =
@@ -440,19 +473,29 @@ class ActivityLaunchAnimator(
progress: Float,
linearProgress: Float
) {
applyStateToWindow(window, state)
// Apply the state to the window only if it is visible, i.e. when the expanding
// view is *not* visible.
if (!state.visible) {
applyStateToWindow(window, state)
}
navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
listeners.forEach { it.onLaunchAnimationProgress(linearProgress) }
delegate.onLaunchAnimationProgress(state, progress, linearProgress)
}
}
// We draw a hole when the additional layer is fading out to reveal the opening window.
animation = launchAnimator.startAnimation(
controller, endState, windowBackgroundColor, drawHole = true)
}
private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) {
if (transactionApplierView.viewRootImpl == null) {
// If the view root we synchronize with was detached, don't apply any transaction
// (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
return
}
val screenBounds = window.screenSpaceBounds
val centerX = (screenBounds.left + screenBounds.right) / 2f
val centerY = (screenBounds.top + screenBounds.bottom) / 2f
@@ -510,6 +553,12 @@ class ActivityLaunchAnimator(
state: LaunchAnimator.State,
linearProgress: Float
) {
if (transactionApplierView.viewRootImpl == null) {
// If the view root we synchronize with was detached, don't apply any transaction
// (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
return
}
val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress,
ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)

View File

@@ -19,7 +19,6 @@ package com.android.systemui.animation
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.app.ActivityManager
import android.app.Dialog
import android.graphics.Color
import android.graphics.Rect
@@ -28,12 +27,12 @@ import android.service.dreams.IDreamManager
import android.util.Log
import android.util.MathUtils
import android.view.GhostView
import android.view.SurfaceControl
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewRootImpl
import android.view.WindowInsets
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
import android.widget.FrameLayout
import kotlin.math.roundToInt
@@ -42,12 +41,17 @@ private const val TAG = "DialogLaunchAnimator"
/**
* A class that allows dialogs to be started in a seamless way from a view that is transforming
* nicely into the starting dialog.
*
* This animator also allows to easily animate a dialog into an activity.
*
* @see showFromView
* @see showFromDialog
* @see createActivityLaunchController
*/
class DialogLaunchAnimator @JvmOverloads constructor(
private val dreamManager: IDreamManager,
private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
// TODO(b/217621394): Remove special handling for low-RAM devices after animation sync is fixed
private var forceDisableSynchronization: Boolean = ActivityManager.isLowRamDeviceStatic()
private val isForTesting: Boolean = false
) {
private companion object {
private val TIMINGS = ActivityLaunchAnimator.TIMINGS
@@ -113,7 +117,7 @@ class DialogLaunchAnimator @JvmOverloads constructor(
dialog = dialog,
animateBackgroundBoundsChange,
animatedParent,
forceDisableSynchronization
isForTesting
)
openedDialogs.add(animatedDialog)
@@ -140,6 +144,100 @@ class DialogLaunchAnimator @JvmOverloads constructor(
showFromView(dialog, view, animateBackgroundBoundsChange)
}
/**
* Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from the
* dialog that contains [View]. Note that the dialog must have been show using [showFromView]
* and be currently showing, otherwise this will return null.
*
* The returned controller will take care of dismissing the dialog at the right time after the
* activity started, when the dialog to app animation is done (or when it is cancelled). If this
* method returns null, then the dialog won't be dismissed.
*
* @param view any view inside the dialog to animate.
*/
@JvmOverloads
fun createActivityLaunchController(
view: View,
cujType: Int? = null
): ActivityLaunchAnimator.Controller? {
val animatedDialog = openedDialogs
.firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl }
?: return null
// At this point, we know that the intent of the caller is to dismiss the dialog to show
// an app, so we disable the exit animation into the touch surface because we will never
// want to run it anyways.
animatedDialog.exitAnimationDisabled = true
val dialog = animatedDialog.dialog
// Don't animate if the dialog is not showing.
if (!dialog.isShowing) {
return null
}
val dialogContentWithBackground = animatedDialog.dialogContentWithBackground ?: return null
val controller =
ActivityLaunchAnimator.Controller.fromView(dialogContentWithBackground, cujType)
?: return null
// Wrap the controller into one that will instantly dismiss the dialog when the animation is
// done or dismiss it normally (fading it out) if the animation is cancelled.
return object : ActivityLaunchAnimator.Controller by controller {
override val isDialogLaunch = true
override fun onIntentStarted(willAnimate: Boolean) {
controller.onIntentStarted(willAnimate)
if (!willAnimate) {
dialog.dismiss()
}
}
override fun onLaunchAnimationCancelled() {
controller.onLaunchAnimationCancelled()
enableDialogDismiss()
dialog.dismiss()
}
override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
controller.onLaunchAnimationStart(isExpandingFullyAbove)
// Make sure the dialog is not dismissed during the animation.
disableDialogDismiss()
// If this dialog was shown from a cascade of other dialogs, make sure those ones
// are dismissed too.
animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss()
// Remove the dim.
dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
}
override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
controller.onLaunchAnimationEnd(isExpandingFullyAbove)
// Hide the dialog then dismiss it to instantly dismiss it without playing the
// animation.
dialog.hide()
enableDialogDismiss()
dialog.dismiss()
}
private fun disableDialogDismiss() {
dialog.setDismissOverride { /* Do nothing */ }
}
private fun enableDialogDismiss() {
// We don't set the override to null given that [AnimatedDialog.OnDialogDismissed]
// will still properly dismiss the dialog but will also make sure to clean up
// everything (like making sure that the touched view that triggered the dialog is
// made VISIBLE again).
dialog.setDismissOverride(animatedDialog::onDialogDismissed)
}
}
}
/**
* Ensure that all dialogs currently shown won't animate into their touch surface when
* dismissed.
@@ -358,6 +456,21 @@ private class AnimatedDialog(
// Make sure the dialog is visible instantly and does not do any window animation.
window.attributes.windowAnimations = R.style.Animation_LaunchAnimation
// Ensure that the animation is not clipped by the display cut-out when animating this
// dialog into an app.
window.attributes.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
window.attributes = window.attributes
// We apply the insets ourselves to make sure that the paddings are set on the correct
// View.
window.setDecorFitsSystemWindows(false)
val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup)
viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsets.Type.displayCutout())
view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
WindowInsets.CONSUMED
}
// Start the animation once the background view is properly laid out.
dialogContentWithBackground.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
@@ -421,45 +534,12 @@ private class AnimatedDialog(
* (or inversely, removed from the UI when the touch surface is made visible).
*/
private fun synchronizeNextDraw(then: () -> Unit) {
if (forceDisableSynchronization ||
!touchSurface.isAttachedToWindow || touchSurface.viewRootImpl == null ||
!decorView.isAttachedToWindow || decorView.viewRootImpl == null) {
// No need to synchronize if either the touch surface or dialog view is not attached
// to a window.
if (forceDisableSynchronization) {
then()
return
}
// Consume the next frames of both view roots to make sure the ghost view is drawn at
// exactly the same time as when the touch surface is made invisible.
var remainingTransactions = 0
val mergedTransactions = SurfaceControl.Transaction()
fun onTransaction(transaction: SurfaceControl.Transaction?) {
remainingTransactions--
transaction?.let { mergedTransactions.merge(it) }
if (remainingTransactions == 0) {
mergedTransactions.apply()
then()
}
}
fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
if (viewRootImpl.consumeNextDraw(::onTransaction)) {
remainingTransactions++
// Make sure we trigger a traversal.
viewRootImpl.view.invalidate()
}
}
consumeNextDraw(touchSurface.viewRootImpl)
consumeNextDraw(decorView.viewRootImpl)
if (remainingTransactions == 0) {
then()
}
ViewRootSync.synchronizeNextDraw(touchSurface, decorView, then)
}
private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
@@ -523,7 +603,7 @@ private class AnimatedDialog(
)
}
private fun onDialogDismissed() {
fun onDialogDismissed() {
if (Looper.myLooper() != Looper.getMainLooper()) {
dialog.context.mainExecutor.execute { onDialogDismissed() }
return

View File

@@ -77,8 +77,8 @@ class LaunchAnimator(
* This will be used to:
* - Get the associated [Context].
* - Compute whether we are expanding fully above the launch container.
* - Apply surface transactions in sync with RenderThread when animating an activity
* launch.
* - Get to overlay to which we initially put the window background layer, until the
* opening window is made visible (see [openingWindowSyncView]).
*
* This container can be changed to force this [Controller] to animate the expanding view
* inside a different location, for instance to ensure correct layering during the
@@ -86,6 +86,18 @@ class LaunchAnimator(
*/
var launchContainer: ViewGroup
/**
* The [View] with which the opening app window should be synchronized with once it starts
* to be visible.
*
* We will also move the window background layer to this view's overlay once the opening
* window is visible.
*
* If null, this will default to [launchContainer].
*/
val openingWindowSyncView: View?
get() = null
/**
* Return the [State] of the view that will be animated. We will animate from this state to
* the final window state.
@@ -100,11 +112,9 @@ class LaunchAnimator(
* needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
* fully above the [launchContainer].
*/
@JvmDefault
fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
/** The animation made progress and the expandable view [state] should be updated. */
@JvmDefault
fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
/**
@@ -112,7 +122,6 @@ class LaunchAnimator(
* called previously. This is typically used to clean up the resources initialized when the
* animation was started.
*/
@JvmDefault
fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
}
@@ -154,7 +163,7 @@ class LaunchAnimator(
}
/** The timings (durations and delays) used by this animator. */
class Timings(
data class Timings(
/** The total duration of the animation. */
val totalDuration: Long,
@@ -257,8 +266,17 @@ class LaunchAnimator(
animator.duration = timings.totalDuration
animator.interpolator = LINEAR
// Whether we should move the [windowBackgroundLayer] into the overlay of
// [Controller.openingWindowSyncView] once the opening app window starts to be visible.
val openingWindowSyncView = controller.openingWindowSyncView
val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
val moveBackgroundLayerWhenAppIsVisible = openingWindowSyncView != null &&
openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl
val launchContainerOverlay = launchContainer.overlay
var cancelled = false
var movedBackgroundLayer = false
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
if (DEBUG) {
@@ -278,6 +296,10 @@ class LaunchAnimator(
}
controller.onLaunchAnimationEnd(isExpandingFullyAbove)
launchContainerOverlay.remove(windowBackgroundLayer)
if (moveBackgroundLayerWhenAppIsVisible) {
openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
}
}
})
@@ -318,11 +340,29 @@ class LaunchAnimator(
timings.contentBeforeFadeOutDuration
) < 1
if (moveBackgroundLayerWhenAppIsVisible && !state.visible && !movedBackgroundLayer) {
// The expanding view is not visible, so the opening app is visible. If this is the
// first frame when it happens, trigger a one-off sync and move the background layer
// in its new container.
movedBackgroundLayer = true
launchContainerOverlay.remove(windowBackgroundLayer)
openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)
ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {})
}
val container = if (movedBackgroundLayer) {
openingWindowSyncView!!
} else {
controller.launchContainer
}
applyStateToWindowBackgroundLayer(
windowBackgroundLayer,
state,
linearProgress,
launchContainer,
container,
drawHole
)
controller.onLaunchAnimationProgress(state, progress, linearProgress)

View File

@@ -0,0 +1,75 @@
package com.android.systemui.animation
import android.app.ActivityManager
import android.view.SurfaceControl
import android.view.View
import android.view.ViewRootImpl
/** A util class to synchronize 2 view roots. */
// TODO(b/200284684): Remove this class.
object ViewRootSync {
// TODO(b/217621394): Remove special handling for low-RAM devices after animation sync is fixed
private val forceDisableSynchronization = ActivityManager.isLowRamDeviceStatic()
/**
* Synchronize the next draw between the view roots of [view] and [otherView], then run [then].
*
* Note that in some cases, the synchronization might not be possible (e.g. WM consumed the
* next transactions) or disabled (temporarily, on low ram devices). In this case, [then] will
* be called without synchronizing.
*/
fun synchronizeNextDraw(
view: View,
otherView: View,
then: () -> Unit
) {
if (forceDisableSynchronization ||
!view.isAttachedToWindow || view.viewRootImpl == null ||
!otherView.isAttachedToWindow || otherView.viewRootImpl == null ||
view.viewRootImpl == otherView.viewRootImpl) {
// No need to synchronize if either the touch surface or dialog view is not attached
// to a window.
then()
return
}
// Consume the next frames of both view roots to make sure the ghost view is drawn at
// exactly the same time as when the touch surface is made invisible.
var remainingTransactions = 0
val mergedTransactions = SurfaceControl.Transaction()
fun onTransaction(transaction: SurfaceControl.Transaction?) {
remainingTransactions--
transaction?.let { mergedTransactions.merge(it) }
if (remainingTransactions == 0) {
mergedTransactions.apply()
then()
}
}
fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
if (viewRootImpl.consumeNextDraw(::onTransaction)) {
remainingTransactions++
// Make sure we trigger a traversal.
viewRootImpl.view.invalidate()
}
}
consumeNextDraw(view.viewRootImpl)
consumeNextDraw(otherView.viewRootImpl)
if (remainingTransactions == 0) {
then()
}
}
/**
* A Java-friendly API for [synchronizeNextDraw].
*/
@JvmStatic
fun synchronizeNextDraw(view: View, otherView: View, then: Runnable) {
synchronizeNextDraw(view, otherView, then::run)
}
}

View File

@@ -204,7 +204,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
d.setColorFilter(new PorterDuffColorFilter(
Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN));
mTitleIcon.setImageDrawable(d);
mContainerLayout.setOnClickListener(v -> onItemClick(CUSTOMIZED_ITEM_PAIR_NEW));
mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
}
}
@@ -241,11 +241,5 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
notifyDataSetChanged();
}
}
private void onItemClick(int customizedItem) {
if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
mController.launchBluetoothPairing();
}
}
}
}

View File

@@ -56,6 +56,7 @@ import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.R;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.monet.ColorScheme;
import com.android.systemui.plugins.ActivityStarter;
@@ -533,12 +534,14 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
return false;
}
void launchBluetoothPairing() {
// Dismissing a dialog into its touch surface and starting an activity at the same time
// looks bad, so let's make sure the dialog just fades out quickly.
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
void launchBluetoothPairing(View view) {
ActivityLaunchAnimator.Controller controller =
mDialogLaunchAnimator.createActivityLaunchController(view);
if (controller == null) {
mCallback.dismissDialog();
}
mCallback.dismissDialog();
Intent launchIntent =
new Intent(ACTION_BLUETOOTH_PAIRING_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
@@ -553,10 +556,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback {
deepLinkIntent.putExtra(
Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY,
PAGE_CONNECTED_DEVICES_KEY);
mActivityStarter.startActivity(deepLinkIntent, true);
mActivityStarter.startActivity(deepLinkIntent, true, controller);
return;
}
mActivityStarter.startActivity(launchIntent, true);
mActivityStarter.startActivity(launchIntent, true, controller);
}
void launchMediaOutputGroupDialog(View mediaOutputDialog) {

View File

@@ -36,6 +36,7 @@ import com.android.internal.app.MediaRouteDialogPresenter;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.R;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -191,10 +192,16 @@ public class CastTile extends QSTileImpl<BooleanState> {
mContext,
ROUTE_TYPE_REMOTE_DISPLAY,
v -> {
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
holder.mDialog.dismiss();
ActivityLaunchAnimator.Controller controller =
mDialogLaunchAnimator.createActivityLaunchController(v);
if (controller == null) {
holder.mDialog.dismiss();
}
mActivityStarter
.postStartActivityDismissingKeyguard(getLongClickIntent(), 0);
.postStartActivityDismissingKeyguard(getLongClickIntent(), 0,
controller);
});
holder.init(dialog);
SystemUIDialog.setShowForAllUsers(dialog, true);

View File

@@ -164,7 +164,7 @@ public class InternetAdapter extends RecyclerView.Adapter<InternetAdapter.Intern
if (connectedState != WifiEntry.CONNECTED_STATE_DISCONNECTED) {
mWifiListLayout.setOnClickListener(
v -> mInternetDialogController.launchWifiNetworkDetailsSetting(
wifiEntry.getKey()));
wifiEntry.getKey(), v));
return;
}
mWifiListLayout.setOnClickListener(v -> {

View File

@@ -355,8 +355,8 @@ public class InternetDialog extends SystemUIDialog implements
isChecked, false);
}
});
mConnectedWifListLayout.setOnClickListener(v -> onClickConnectedWifi());
mSeeAllLayout.setOnClickListener(v -> onClickSeeMoreButton());
mConnectedWifListLayout.setOnClickListener(this::onClickConnectedWifi);
mSeeAllLayout.setOnClickListener(this::onClickSeeMoreButton);
mWiFiToggle.setOnCheckedChangeListener(
(buttonView, isChecked) -> {
if (mWifiManager == null) return;
@@ -519,7 +519,7 @@ public class InternetDialog extends SystemUIDialog implements
if (TextUtils.isEmpty(mWifiScanNotifyText.getText())) {
final AnnotationLinkSpan.LinkInfo linkInfo = new AnnotationLinkSpan.LinkInfo(
AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION,
v -> mInternetDialogController.launchWifiScanningSetting());
mInternetDialogController::launchWifiScanningSetting);
mWifiScanNotifyText.setText(AnnotationLinkSpan.linkify(
getContext().getText(R.string.wifi_scan_notify_message), linkInfo));
mWifiScanNotifyText.setMovementMethod(LinkMovementMethod.getInstance());
@@ -527,15 +527,16 @@ public class InternetDialog extends SystemUIDialog implements
mWifiScanNotifyLayout.setVisibility(View.VISIBLE);
}
void onClickConnectedWifi() {
void onClickConnectedWifi(View view) {
if (mConnectedWifiEntry == null) {
return;
}
mInternetDialogController.launchWifiNetworkDetailsSetting(mConnectedWifiEntry.getKey());
mInternetDialogController.launchWifiNetworkDetailsSetting(mConnectedWifiEntry.getKey(),
view);
}
void onClickSeeMoreButton() {
mInternetDialogController.launchNetworkSetting();
void onClickSeeMoreButton(View view) {
mInternetDialogController.launchNetworkSetting(view);
}
CharSequence getDialogTitleText() {

View File

@@ -72,6 +72,7 @@ import com.android.settingslib.mobile.TelephonyIcons;
import com.android.settingslib.net.SignalStrengthUtil;
import com.android.settingslib.wifi.WifiUtils;
import com.android.systemui.R;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
@@ -620,36 +621,32 @@ public class InternetDialogController implements AccessPointController.AccessPoi
return summary;
}
void launchNetworkSetting() {
// Dismissing a dialog into its touch surface and starting an activity at the same time
// looks bad, so let's make sure the dialog just fades out quickly.
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
mCallback.dismissDialog();
private void startActivity(Intent intent, View view) {
ActivityLaunchAnimator.Controller controller =
mDialogLaunchAnimator.createActivityLaunchController(view);
mActivityStarter.postStartActivityDismissingKeyguard(getSettingsIntent(), 0);
if (controller == null) {
mCallback.dismissDialog();
}
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0, controller);
}
void launchWifiNetworkDetailsSetting(String key) {
void launchNetworkSetting(View view) {
startActivity(getSettingsIntent(), view);
}
void launchWifiNetworkDetailsSetting(String key, View view) {
Intent intent = getWifiDetailsSettingsIntent(key);
if (intent != null) {
// Dismissing a dialog into its touch surface and starting an activity at the same time
// looks bad, so let's make sure the dialog just fades out quickly.
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
mCallback.dismissDialog();
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
startActivity(intent, view);
}
}
void launchWifiScanningSetting() {
// Dismissing a dialog into its touch surface and starting an activity at the same time
// looks bad, so let's make sure the dialog just fades out quickly.
mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
mCallback.dismissDialog();
void launchWifiScanningSetting(View view) {
final Intent intent = new Intent(ACTION_WIFI_SCANNING_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
startActivity(intent, view);
}
void connectCarrierNetwork() {

View File

@@ -19,6 +19,7 @@ package com.android.systemui.qs.user
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.content.DialogInterface.BUTTON_NEUTRAL
import android.content.Intent
import android.provider.Settings
import android.view.LayoutInflater
@@ -84,16 +85,20 @@ class UserSwitchDialogController @VisibleForTesting constructor(
setPositiveButton(R.string.quick_settings_done) { _, _ ->
uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_CLOSE)
}
setNeutralButton(R.string.quick_settings_more_user_settings) { _, _ ->
setNeutralButton(R.string.quick_settings_more_user_settings, { _, _ ->
if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
uiEventLogger.log(QSUserSwitcherEvent.QS_USER_MORE_SETTINGS)
val controller = dialogLaunchAnimator.createActivityLaunchController(
getButton(BUTTON_NEUTRAL))
if (controller == null) {
dismiss()
}
activityStarter.postStartActivityDismissingKeyguard(
USER_SETTINGS_INTENT,
0
)
USER_SETTINGS_INTENT, 0, controller)
}
}
}, false /* dismissOnClick */)
val gridFrame = LayoutInflater.from(this.context)
.inflate(R.layout.qs_user_dialog_content, null)
setView(gridFrame)

View File

@@ -2580,9 +2580,9 @@ public class StatusBar extends CoreStartable implements
return controllerFromStatusBar.get();
}
if (dismissShade && rootView == mNotificationShadeWindowView) {
// We are animating a view in the shade. We have to make sure that we collapse it when
// the animation ends or is cancelled.
if (dismissShade) {
// If the view is not in the status bar, then we are animating a view in the shade.
// We have to make sure that we collapse it when the animation ends or is cancelled.
return new StatusBarLaunchAnimatorController(animationController, this,
true /* isLaunchForActivity */);
}

View File

@@ -1,5 +1,6 @@
package com.android.systemui.statusbar.phone
import android.view.View
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.LaunchAnimator
@@ -12,6 +13,11 @@ class StatusBarLaunchAnimatorController(
private val statusBar: StatusBar,
private val isLaunchForActivity: Boolean = true
) : ActivityLaunchAnimator.Controller by delegate {
// Always sync the opening window with the shade, given that we draw a hole punch in the shade
// of the same size and position as the opening app to make it visible.
override val openingWindowSyncView: View?
get() = statusBar.notificationShadeWindowView
override fun onIntentStarted(willAnimate: Boolean) {
delegate.onIntentStarted(willAnimate)
if (!willAnimate) {

View File

@@ -44,6 +44,9 @@ import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import java.util.ArrayList;
import java.util.List;
/**
* Base class for dialogs that should appear over panels and keyguard.
*
@@ -68,6 +71,8 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh
private int mLastConfigurationWidthDp = -1;
private int mLastConfigurationHeightDp = -1;
private List<Runnable> mOnCreateRunnables = new ArrayList<>();
public SystemUIDialog(Context context) {
this(context, R.style.Theme_SystemUI_Dialog);
}
@@ -110,6 +115,10 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh
mLastConfigurationWidthDp = config.screenWidthDp;
mLastConfigurationHeightDp = config.screenHeightDp;
updateWindowSize();
for (int i = 0; i < mOnCreateRunnables.size(); i++) {
mOnCreateRunnables.get(i).run();
}
}
private void updateWindowSize() {
@@ -197,16 +206,67 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh
setMessage(mContext.getString(resId));
}
/**
* Set a listener to be invoked when the positive button of the dialog is pressed. The dialog
* will automatically be dismissed when the button is clicked.
*/
public void setPositiveButton(int resId, OnClickListener onClick) {
setButton(BUTTON_POSITIVE, mContext.getString(resId), onClick);
setPositiveButton(resId, onClick, true /* dismissOnClick */);
}
/**
* Set a listener to be invoked when the positive button of the dialog is pressed. The dialog
* will be dismissed when the button is clicked iff {@code dismissOnClick} is true.
*/
public void setPositiveButton(int resId, OnClickListener onClick, boolean dismissOnClick) {
setButton(BUTTON_POSITIVE, resId, onClick, dismissOnClick);
}
/**
* Set a listener to be invoked when the negative button of the dialog is pressed. The dialog
* will automatically be dismissed when the button is clicked.
*/
public void setNegativeButton(int resId, OnClickListener onClick) {
setButton(BUTTON_NEGATIVE, mContext.getString(resId), onClick);
setNegativeButton(resId, onClick, true /* dismissOnClick */);
}
/**
* Set a listener to be invoked when the negative button of the dialog is pressed. The dialog
* will be dismissed when the button is clicked iff {@code dismissOnClick} is true.
*/
public void setNegativeButton(int resId, OnClickListener onClick, boolean dismissOnClick) {
setButton(BUTTON_NEGATIVE, resId, onClick, dismissOnClick);
}
/**
* Set a listener to be invoked when the neutral button of the dialog is pressed. The dialog
* will automatically be dismissed when the button is clicked.
*/
public void setNeutralButton(int resId, OnClickListener onClick) {
setButton(BUTTON_NEUTRAL, mContext.getString(resId), onClick);
setNeutralButton(resId, onClick, true /* dismissOnClick */);
}
/**
* Set a listener to be invoked when the neutral button of the dialog is pressed. The dialog
* will be dismissed when the button is clicked iff {@code dismissOnClick} is true.
*/
public void setNeutralButton(int resId, OnClickListener onClick, boolean dismissOnClick) {
setButton(BUTTON_NEUTRAL, resId, onClick, dismissOnClick);
}
private void setButton(int whichButton, int resId, OnClickListener onClick,
boolean dismissOnClick) {
if (dismissOnClick) {
setButton(whichButton, mContext.getString(resId), onClick);
} else {
// Set a null OnClickListener to make sure the button is still created and shown.
setButton(whichButton, mContext.getString(resId), (OnClickListener) null);
// When the dialog is created, set the click listener but don't dismiss the dialog when
// it is clicked.
mOnCreateRunnables.add(() -> getButton(whichButton).setOnClickListener(
view -> onClick.onClick(this, whichButton)));
}
}
public static void setShowForAllUsers(Dialog dialog, boolean show) {

View File

@@ -46,7 +46,7 @@ import kotlin.concurrent.thread
@RunWithLooper
class ActivityLaunchAnimatorTest : SysuiTestCase() {
private val launchContainer = LinearLayout(mContext)
private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
private val testLaunchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
@Mock lateinit var callback: ActivityLaunchAnimator.Callback
@Mock lateinit var listener: ActivityLaunchAnimator.Listener
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
@@ -58,7 +58,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
@Before
fun setup() {
activityLaunchAnimator = ActivityLaunchAnimator(launchAnimator)
activityLaunchAnimator = ActivityLaunchAnimator(testLaunchAnimator, testLaunchAnimator)
activityLaunchAnimator.callback = callback
activityLaunchAnimator.addListener(listener)
}
@@ -129,13 +129,11 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
@Test
fun animatesIfActivityIsAlreadyOpenAndIsOnKeyguard() {
`when`(callback.isOnKeyguard()).thenReturn(true)
val animator = ActivityLaunchAnimator(launchAnimator)
animator.callback = callback
val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
var animationAdapter: RemoteAnimationAdapter? = null
startIntentWithAnimation(animator) { adapter ->
startIntentWithAnimation(activityLaunchAnimator) { adapter ->
animationAdapter = adapter
ActivityManager.START_DELIVERED_TO_TOP
}

View File

@@ -20,6 +20,7 @@ import com.android.systemui.SysuiTestCase
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertNotNull
import junit.framework.Assert.assertNull
import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Before
@@ -43,7 +44,7 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
@Before
fun setUp() {
dialogLaunchAnimator = DialogLaunchAnimator(
dreamManager, launchAnimator, forceDisableSynchronization = true)
dreamManager, launchAnimator, isForTesting = true)
}
@After
@@ -92,11 +93,6 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
// Clicking the transparent background should dismiss the dialog.
runOnMainThreadAndWaitForIdleSync {
// TODO(b/204561691): Remove this call to disableAllCurrentDialogsExitAnimations() and
// make sure that the test still pass on git_master/cf_x86_64_phone-userdebug in
// Forrest.
dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
transparentBackground.performClick()
}
assertFalse(dialog.isShowing)
@@ -110,7 +106,6 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
assertTrue(firstDialog.isShowing)
assertTrue(secondDialog.isShowing)
runOnMainThreadAndWaitForIdleSync {
dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
dialogLaunchAnimator.dismissStack(secondDialog)
}
@@ -118,6 +113,38 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
assertFalse(secondDialog.isShowing)
}
@Test
fun testActivityLaunchControllerFromDialog() {
val firstDialog = createAndShowDialog()
val secondDialog = createDialogAndShowFromDialog(firstDialog)
val controller =
dialogLaunchAnimator.createActivityLaunchController(secondDialog.contentView)!!
// The dialog shouldn't be dismissable during the animation.
runOnMainThreadAndWaitForIdleSync {
controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
secondDialog.dismiss()
}
assertTrue(secondDialog.isShowing)
// Both dialogs should be dismissed at the end of the animation.
runOnMainThreadAndWaitForIdleSync {
controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
}
assertFalse(firstDialog.isShowing)
assertFalse(secondDialog.isShowing)
}
@Test
fun testActivityLaunchFromHiddenDialog() {
val dialog = createAndShowDialog()
runOnMainThreadAndWaitForIdleSync {
dialog.hide()
}
assertNull(dialogLaunchAnimator.createActivityLaunchController(dialog.contentView))
}
private fun createAndShowDialog(): TestDialog {
return runOnMainThreadAndWaitForIdleSync {
val touchSurfaceRoot = LinearLayout(context)

View File

@@ -59,6 +59,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
private Icon mIcon = mock(Icon.class);
private IconCompat mIconCompat = mock(IconCompat.class);
private View mDialogLaunchView = mock(View.class);
private MediaOutputAdapter mMediaOutputAdapter;
private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
@@ -245,7 +246,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
mViewHolder.mContainerLayout.performClick();
verify(mMediaOutputController).launchBluetoothPairing();
verify(mMediaOutputController).launchBluetoothPairing(mViewHolder.mContainerLayout);
}
@Test

View File

@@ -136,6 +136,8 @@ public class InternetDialogControllerTest extends SysuiTestCase {
private LocationController mLocationController;
@Mock
private DialogLaunchAnimator mDialogLaunchAnimator;
@Mock
private View mDialogLaunchView;
private TestableResources mTestableResources;
private InternetDialogController mInternetDialogController;
@@ -384,7 +386,8 @@ public class InternetDialogControllerTest extends SysuiTestCase {
@Test
public void launchWifiNetworkDetailsSetting_withNoWifiEntryKey_doNothing() {
mInternetDialogController.launchWifiNetworkDetailsSetting(null /* key */);
mInternetDialogController.launchWifiNetworkDetailsSetting(null /* key */,
mDialogLaunchView);
verify(mActivityStarter, never())
.postStartActivityDismissingKeyguard(any(Intent.class), anyInt());
@@ -392,9 +395,11 @@ public class InternetDialogControllerTest extends SysuiTestCase {
@Test
public void launchWifiNetworkDetailsSetting_withWifiEntryKey_startActivity() {
mInternetDialogController.launchWifiNetworkDetailsSetting("wifi_entry_key");
mInternetDialogController.launchWifiNetworkDetailsSetting("wifi_entry_key",
mDialogLaunchView);
verify(mActivityStarter).postStartActivityDismissingKeyguard(any(Intent.class), anyInt());
verify(mActivityStarter).postStartActivityDismissingKeyguard(any(Intent.class), anyInt(),
any());
}
@Test

View File

@@ -458,7 +458,8 @@ public class InternetDialogTest extends SysuiTestCase {
public void onClickSeeMoreButton_clickSeeAll_verifyLaunchNetworkSetting() {
mSeeAll.performClick();
verify(mInternetDialogController).launchNetworkSetting();
verify(mInternetDialogController).launchNetworkSetting(
mDialogView.requireViewById(R.id.see_all_layout));
}
@Test

View File

@@ -21,6 +21,7 @@ import android.content.Intent
import android.provider.Settings
import android.testing.AndroidTestingRunner
import android.view.View
import android.widget.Button
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
@@ -63,6 +64,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
@Mock
private lateinit var launchView: View
@Mock
private lateinit var neutralButton: Button
@Mock
private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
@Mock
private lateinit var uiEventLogger: UiEventLogger
@@ -130,14 +133,17 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
controller.showDialog(launchView)
verify(dialog).setNeutralButton(anyInt(), capture(clickCaptor))
verify(dialog)
.setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
`when`(dialog.getButton(DialogInterface.BUTTON_NEUTRAL)).thenReturn(neutralButton)
clickCaptor.value.onClick(dialog, DialogInterface.BUTTON_NEUTRAL)
verify(activityStarter)
.postStartActivityDismissingKeyguard(
argThat(IntentMatcher(Settings.ACTION_USER_SETTINGS)),
eq(0)
eq(0),
eq(null)
)
verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_MORE_SETTINGS)
}
@@ -148,7 +154,8 @@ class UserSwitchDialogControllerTest : SysuiTestCase() {
controller.showDialog(launchView)
verify(dialog).setNeutralButton(anyInt(), capture(clickCaptor))
verify(dialog)
.setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
clickCaptor.value.onClick(dialog, DialogInterface.BUTTON_NEUTRAL)