Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e6106974 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Use Expandable instead of View in FooterActionsViewModel (1/2)

This CL replaces the reference to View by Expandable in
FooterActionsViewModel. That way, this view model can now be used by
Compose code as well.

Bug: 242040009
Test: atest FooterActionsViewModelTest
Change-Id: I6674d9a39d5daf699d67f1bb8e8ae317c85e9240
parent 5d83ecce
Loading
Loading
Loading
Loading
+42 −94
Original line number Original line Diff line number Diff line
@@ -25,7 +25,6 @@ import android.graphics.Rect
import android.os.Looper
import android.os.Looper
import android.util.Log
import android.util.Log
import android.util.MathUtils
import android.util.MathUtils
import android.view.GhostView
import android.view.View
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@@ -86,6 +85,9 @@ constructor(
         */
         */
        val sourceIdentity: Any
        val sourceIdentity: Any


        /** The CUJ associated to this controller. */
        val cuj: DialogCuj?

        /**
        /**
         * Move the drawing of the source in the overlay of [viewGroup].
         * Move the drawing of the source in the overlay of [viewGroup].
         *
         *
@@ -142,7 +144,31 @@ constructor(
         * controlled by this controller.
         * controlled by this controller.
         */
         */
        // TODO(b/252723237): Make this non-nullable
        // TODO(b/252723237): Make this non-nullable
        fun jankConfigurationBuilder(cuj: Int): InteractionJankMonitor.Configuration.Builder?
        fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder?

        companion object {
            /**
             * Create a [Controller] that can animate [source] to and from a dialog.
             *
             * Important: The view must be attached to a [ViewGroup] when calling this function and
             * during the animation. For safety, this method will return null when it is not.
             *
             * Note: The background of [view] should be a (rounded) rectangle so that it can be
             * properly animated.
             */
            fun fromView(source: View, cuj: DialogCuj? = null): Controller? {
                if (source.parent !is ViewGroup) {
                    Log.e(
                        TAG,
                        "Skipping animation as view $source is not attached to a ViewGroup",
                        Exception(),
                    )
                    return null
                }

                return ViewDialogLaunchAnimatorController(source, cuj)
            }
        }
    }
    }


    /**
    /**
@@ -172,7 +198,12 @@ constructor(
        cuj: DialogCuj? = null,
        cuj: DialogCuj? = null,
        animateBackgroundBoundsChange: Boolean = false
        animateBackgroundBoundsChange: Boolean = false
    ) {
    ) {
        show(dialog, createController(view), cuj, animateBackgroundBoundsChange)
        val controller = Controller.fromView(view, cuj)
        if (controller == null) {
            dialog.show()
        } else {
            show(dialog, controller, animateBackgroundBoundsChange)
        }
    }
    }


    /**
    /**
@@ -187,10 +218,10 @@ constructor(
     * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
     * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
     * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
     * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
     */
     */
    @JvmOverloads
    fun show(
    fun show(
        dialog: Dialog,
        dialog: Dialog,
        controller: Controller,
        controller: Controller,
        cuj: DialogCuj? = null,
        animateBackgroundBoundsChange: Boolean = false
        animateBackgroundBoundsChange: Boolean = false
    ) {
    ) {
        if (Looper.myLooper() != Looper.getMainLooper()) {
        if (Looper.myLooper() != Looper.getMainLooper()) {
@@ -207,7 +238,10 @@ constructor(
                it.dialog.window.decorView.viewRootImpl == controller.viewRoot
                it.dialog.window.decorView.viewRootImpl == controller.viewRoot
            }
            }
        val animateFrom =
        val animateFrom =
            animatedParent?.dialogContentWithBackground?.let { createController(it) } ?: controller
            animatedParent?.dialogContentWithBackground?.let {
                Controller.fromView(it, controller.cuj)
            }
                ?: controller


        if (animatedParent == null && animateFrom !is LaunchableView) {
        if (animatedParent == null && animateFrom !is LaunchableView) {
            // Make sure the View we launch from implements LaunchableView to avoid visibility
            // Make sure the View we launch from implements LaunchableView to avoid visibility
@@ -244,96 +278,12 @@ constructor(
                animateBackgroundBoundsChange,
                animateBackgroundBoundsChange,
                animatedParent,
                animatedParent,
                isForTesting,
                isForTesting,
                cuj,
            )
            )


        openedDialogs.add(animatedDialog)
        openedDialogs.add(animatedDialog)
        animatedDialog.start()
        animatedDialog.start()
    }
    }


    /** Create a [Controller] that can animate [source] to & from a dialog. */
    private fun createController(source: View): Controller {
        return object : Controller {
            override val viewRoot: ViewRootImpl
                get() = source.viewRootImpl

            override val sourceIdentity: Any = source

            override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
                // Create a temporary ghost of the source (which will make it invisible) and add it
                // to the host dialog.
                GhostView.addGhost(source, viewGroup)

                // The ghost of the source was just created, so the source is currently invisible.
                // We need to make sure that it stays invisible as long as the dialog is shown or
                // animating.
                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
            }

            override fun stopDrawingInOverlay() {
                // Note: here we should remove the ghost from the overlay, but in practice this is
                // already done by the launch controllers created below.

                // Make sure we allow the source to change its visibility again.
                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
                source.visibility = View.VISIBLE
            }

            override fun createLaunchController(): LaunchAnimator.Controller {
                val delegate = GhostedViewLaunchAnimatorController(source)
                return object : LaunchAnimator.Controller by delegate {
                    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
                        // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
                        // ghost (that ghosts only the source content, and not its background) will
                        // be added right after this by the delegate and will be animated.
                        GhostView.removeGhost(source)
                        delegate.onLaunchAnimationStart(isExpandingFullyAbove)
                    }

                    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
                        delegate.onLaunchAnimationEnd(isExpandingFullyAbove)

                        // We hide the source when the dialog is showing. We will make this view
                        // visible again when dismissing the dialog. This does nothing if the source
                        // implements [LaunchableView], as it's already INVISIBLE in that case.
                        source.visibility = View.INVISIBLE
                    }
                }
            }

            override fun createExitController(): LaunchAnimator.Controller {
                return GhostedViewLaunchAnimatorController(source)
            }

            override fun shouldAnimateExit(): Boolean {
                // The source 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 (source.visibility != View.INVISIBLE) {
                    return false
                }

                return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
            }

            override fun onExitAnimationCancelled() {
                // Make sure we allow the source to change its visibility again.
                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)

                // If the view is invisible it's probably because of us, so we make it visible
                // again.
                if (source.visibility == View.INVISIBLE) {
                    source.visibility = View.VISIBLE
                }
            }

            override fun jankConfigurationBuilder(
                cuj: Int
            ): InteractionJankMonitor.Configuration.Builder? {
                return InteractionJankMonitor.Configuration.Builder.withView(cuj, source)
            }
        }
    }

    /**
    /**
     * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will
     * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will
     * allow for dismissing the whole stack.
     * allow for dismissing the whole stack.
@@ -563,9 +513,6 @@ private class AnimatedDialog(
     * Whether synchronization should be disabled, which can be useful if we are running in a test.
     * Whether synchronization should be disabled, which can be useful if we are running in a test.
     */
     */
    private val forceDisableSynchronization: Boolean,
    private val forceDisableSynchronization: Boolean,

    /** Interaction to which the dialog animation is associated. */
    private val cuj: DialogCuj? = null
) {
) {
    /**
    /**
     * The DecorView of this dialog window.
     * The DecorView of this dialog window.
@@ -618,8 +565,9 @@ private class AnimatedDialog(
    private var hasInstrumentedJank = false
    private var hasInstrumentedJank = false


    fun start() {
    fun start() {
        val cuj = controller.cuj
        if (cuj != null) {
        if (cuj != null) {
            val config = controller.jankConfigurationBuilder(cuj.cujType)
            val config = controller.jankConfigurationBuilder()
            if (config != null) {
            if (config != null) {
                if (cuj.tag != null) {
                if (cuj.tag != null) {
                    config.setTag(cuj.tag)
                    config.setTag(cuj.tag)
@@ -917,7 +865,7 @@ private class AnimatedDialog(
                }
                }


                if (hasInstrumentedJank) {
                if (hasInstrumentedJank) {
                    interactionJankMonitor.end(cuj!!.cujType)
                    interactionJankMonitor.end(controller.cuj!!.cujType)
                }
                }
            }
            }
        )
        )
+13 −1
Original line number Original line Diff line number Diff line
@@ -30,7 +30,12 @@ interface Expandable {
     */
     */
    fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller?
    fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller?


    // TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here.
    /**
     * Create a [DialogLaunchAnimator.Controller] that can be used to expand this [Expandable] into
     * a Dialog, or return `null` if this [Expandable] should not be animated (e.g. if it is
     * currently not attached or visible).
     */
    fun dialogLaunchController(cuj: DialogCuj? = null): DialogLaunchAnimator.Controller?


    companion object {
    companion object {
        /**
        /**
@@ -39,6 +44,7 @@ interface Expandable {
         * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
         * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
         * animated.
         * animated.
         */
         */
        @JvmStatic
        fun fromView(view: View): Expandable {
        fun fromView(view: View): Expandable {
            return object : Expandable {
            return object : Expandable {
                override fun activityLaunchController(
                override fun activityLaunchController(
@@ -46,6 +52,12 @@ interface Expandable {
                ): ActivityLaunchAnimator.Controller? {
                ): ActivityLaunchAnimator.Controller? {
                    return ActivityLaunchAnimator.Controller.fromView(view, cujType)
                    return ActivityLaunchAnimator.Controller.fromView(view, cujType)
                }
                }

                override fun dialogLaunchController(
                    cuj: DialogCuj?
                ): DialogLaunchAnimator.Controller? {
                    return DialogLaunchAnimator.Controller.fromView(view, cuj)
                }
            }
            }
        }
        }
    }
    }
+107 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2022 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.view.GhostView
import android.view.View
import android.view.ViewGroup
import android.view.ViewRootImpl
import com.android.internal.jank.InteractionJankMonitor

/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */
class ViewDialogLaunchAnimatorController
internal constructor(
    private val source: View,
    override val cuj: DialogCuj?,
) : DialogLaunchAnimator.Controller {
    override val viewRoot: ViewRootImpl
        get() = source.viewRootImpl

    override val sourceIdentity: Any = source

    override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
        // Create a temporary ghost of the source (which will make it invisible) and add it
        // to the host dialog.
        GhostView.addGhost(source, viewGroup)

        // The ghost of the source was just created, so the source is currently invisible.
        // We need to make sure that it stays invisible as long as the dialog is shown or
        // animating.
        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
    }

    override fun stopDrawingInOverlay() {
        // Note: here we should remove the ghost from the overlay, but in practice this is
        // already done by the launch controllers created below.

        // Make sure we allow the source to change its visibility again.
        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
        source.visibility = View.VISIBLE
    }

    override fun createLaunchController(): LaunchAnimator.Controller {
        val delegate = GhostedViewLaunchAnimatorController(source)
        return object : LaunchAnimator.Controller by delegate {
            override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
                // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
                // ghost (that ghosts only the source content, and not its background) will
                // be added right after this by the delegate and will be animated.
                GhostView.removeGhost(source)
                delegate.onLaunchAnimationStart(isExpandingFullyAbove)
            }

            override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
                delegate.onLaunchAnimationEnd(isExpandingFullyAbove)

                // We hide the source when the dialog is showing. We will make this view
                // visible again when dismissing the dialog. This does nothing if the source
                // implements [LaunchableView], as it's already INVISIBLE in that case.
                source.visibility = View.INVISIBLE
            }
        }
    }

    override fun createExitController(): LaunchAnimator.Controller {
        return GhostedViewLaunchAnimatorController(source)
    }

    override fun shouldAnimateExit(): Boolean {
        // The source 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 (source.visibility != View.INVISIBLE) {
            return false
        }

        return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
    }

    override fun onExitAnimationCancelled() {
        // Make sure we allow the source to change its visibility again.
        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)

        // If the view is invisible it's probably because of us, so we make it visible
        // again.
        if (source.visibility == View.INVISIBLE) {
            source.visibility = View.VISIBLE
        }
    }

    override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
        val type = cuj?.cujType ?: return null
        return InteractionJankMonitor.Configuration.Builder.withView(type, source)
    }
}
+28 −17
Original line number Original line Diff line number Diff line
@@ -40,17 +40,16 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.LayoutDirection
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.DialogCuj
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.LaunchAnimator
import com.android.systemui.animation.LaunchAnimator
import kotlin.math.roundToInt
import kotlin.math.roundToInt


/** A controller that can control animated launches. */
/** A controller that can control animated launches from an [Expandable]. */
interface ExpandableController {
interface ExpandableController {
    /** Create an [ActivityLaunchAnimator.Controller] to animate into an Activity. */
    /** The [Expandable] controlled by this controller. */
    fun forActivity(): ActivityLaunchAnimator.Controller
    val expandable: Expandable

    /** Create a [DialogLaunchAnimator.Controller] to animate into a Dialog. */
    fun forDialog(): DialogLaunchAnimator.Controller
}
}


/**
/**
@@ -120,12 +119,25 @@ internal class ExpandableControllerImpl(
    private val layoutDirection: LayoutDirection,
    private val layoutDirection: LayoutDirection,
    private val isComposed: State<Boolean>,
    private val isComposed: State<Boolean>,
) : ExpandableController {
) : ExpandableController {
    override fun forActivity(): ActivityLaunchAnimator.Controller {
    override val expandable: Expandable =
        return activityController()
        object : Expandable {
            override fun activityLaunchController(
                cujType: Int?,
            ): ActivityLaunchAnimator.Controller? {
                if (!isComposed.value) {
                    return null
                }

                return activityController(cujType)
            }
            }


    override fun forDialog(): DialogLaunchAnimator.Controller {
            override fun dialogLaunchController(cuj: DialogCuj?): DialogLaunchAnimator.Controller? {
        return dialogController()
                if (!isComposed.value) {
                    return null
                }

                return dialogController(cuj)
            }
        }
        }


    /**
    /**
@@ -233,7 +245,7 @@ internal class ExpandableControllerImpl(
    }
    }


    /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */
    /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */
    private fun activityController(): ActivityLaunchAnimator.Controller {
    private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller {
        val delegate = launchController()
        val delegate = launchController()
        return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate {
        return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate {
            override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
            override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -248,10 +260,11 @@ internal class ExpandableControllerImpl(
        }
        }
    }
    }


    private fun dialogController(): DialogLaunchAnimator.Controller {
    private fun dialogController(cuj: DialogCuj?): DialogLaunchAnimator.Controller {
        return object : DialogLaunchAnimator.Controller {
        return object : DialogLaunchAnimator.Controller {
            override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl
            override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl
            override val sourceIdentity: Any = this@ExpandableControllerImpl
            override val sourceIdentity: Any = this@ExpandableControllerImpl
            override val cuj: DialogCuj? = cuj


            override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
            override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
                val newOverlay = viewGroup.overlay as ViewGroupOverlay
                val newOverlay = viewGroup.overlay as ViewGroupOverlay
@@ -294,9 +307,7 @@ internal class ExpandableControllerImpl(
                isDialogShowing.value = false
                isDialogShowing.value = false
            }
            }


            override fun jankConfigurationBuilder(
            override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
                cuj: Int
            ): InteractionJankMonitor.Configuration.Builder? {
                // TODO(b/252723237): Add support for jank monitoring when animating from a
                // TODO(b/252723237): Add support for jank monitoring when animating from a
                // Composable.
                // Composable.
                return null
                return null
+12 −8
Original line number Original line Diff line number Diff line
@@ -116,6 +116,7 @@ import com.android.systemui.MultiListLayout;
import com.android.systemui.MultiListLayout.MultiListAdapter;
import com.android.systemui.MultiListLayout.MultiListAdapter;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.animation.Expandable;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -448,10 +449,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
     *
     *
     * @param keyguardShowing     True if keyguard is showing
     * @param keyguardShowing     True if keyguard is showing
     * @param isDeviceProvisioned True if device is provisioned
     * @param isDeviceProvisioned True if device is provisioned
     * @param view                The view from which we should animate the dialog when showing it
     * @param expandable          The expandable from which we should animate the dialog when
     *                            showing it
     */
     */
    public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned,
    public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned,
            @Nullable View view) {
            @Nullable Expandable expandable) {
        mKeyguardShowing = keyguardShowing;
        mKeyguardShowing = keyguardShowing;
        mDeviceProvisioned = isDeviceProvisioned;
        mDeviceProvisioned = isDeviceProvisioned;
        if (mDialog != null && mDialog.isShowing()) {
        if (mDialog != null && mDialog.isShowing()) {
@@ -463,7 +465,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
            mDialog.dismiss();
            mDialog.dismiss();
            mDialog = null;
            mDialog = null;
        } else {
        } else {
            handleShow(view);
            handleShow(expandable);
        }
        }
    }
    }


@@ -495,7 +497,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
        }
        }
    }
    }


    protected void handleShow(@Nullable View view) {
    protected void handleShow(@Nullable Expandable expandable) {
        awakenIfNecessary();
        awakenIfNecessary();
        mDialog = createDialog();
        mDialog = createDialog();
        prepareDialog();
        prepareDialog();
@@ -507,10 +509,12 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene
        // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports
        // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports
        mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
        mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);


        if (view != null) {
        DialogLaunchAnimator.Controller controller =
            mDialogLaunchAnimator.showFromView(mDialog, view,
                expandable != null ? expandable.dialogLaunchController(
                        new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
                        new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
                            INTERACTION_JANK_TAG));
                                INTERACTION_JANK_TAG)) : null;
        if (controller != null) {
            mDialogLaunchAnimator.show(mDialog, controller);
        } else {
        } else {
            mDialog.show();
            mDialog.show();
        }
        }
Loading