SpecialEffectsController.kt

/*
 * Copyright 2019 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 androidx.fragment.app

import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.core.os.CancellationSignal
import androidx.core.view.ViewCompat
import androidx.fragment.R
import androidx.fragment.app.SpecialEffectsController.Operation.State.Companion.asOperationState

/**
 * Controller for all "special effects" (such as Animation, Animator, framework Transition, and
 * AndroidX Transition) that can be applied to a Fragment as part of the addition or removal
 * of that Fragment from its container.
 *
 * Each SpecialEffectsController is responsible for a single [ViewGroup] container.
 */
internal abstract class SpecialEffectsController(val container: ViewGroup) {
    private val pendingOperations = mutableListOf<Operation>()
    private val runningOperations = mutableListOf<Operation>()
    private var operationDirectionIsPop = false
    private var isContainerPostponed = false

    /**
     * Checks what [lifecycle impact][Operation.LifecycleImpact] of special effect for the
     * given FragmentStateManager is still awaiting completion (or cancellation).
     *
     * This could be because the Operation is still pending (and
     * [executePendingOperations] hasn't been called) or because all
     * [started special effects][Operation.markStartedSpecialEffect]
     * haven't [completed][Operation.completeSpecialEffect].
     *
     * @param fragmentStateManager the FragmentStateManager to check for
     * @return The [Operation.LifecycleImpact] of the awaiting Operation, or null if there is
     * no special effects still in progress.
     */
    fun getAwaitingCompletionLifecycleImpact(
        fragmentStateManager: FragmentStateManager
    ): Operation.LifecycleImpact? {
        val fragment = fragmentStateManager.fragment
        val pendingLifecycleImpact = findPendingOperation(fragment)?.lifecycleImpact
        val runningLifecycleImpact = findRunningOperation(fragment)?.lifecycleImpact
        // Only use the running operation if the pending operation is null or NONE
        return when (pendingLifecycleImpact) {
            null -> runningLifecycleImpact
            Operation.LifecycleImpact.NONE -> runningLifecycleImpact
            else -> pendingLifecycleImpact
        }
    }

    private fun findPendingOperation(
        fragment: Fragment
    ) = pendingOperations.firstOrNull { operation ->
        operation.fragment == fragment && !operation.isCanceled
    }

    private fun findRunningOperation(
        fragment: Fragment
    ) = runningOperations.firstOrNull { operation ->
        operation.fragment == fragment && !operation.isCanceled
    }

    fun enqueueAdd(
        finalState: Operation.State,
        fragmentStateManager: FragmentStateManager
    ) {
        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
            Log.v(
                FragmentManager.TAG,
                "SpecialEffectsController: Enqueuing add operation for fragment " +
                    fragmentStateManager.fragment
            )
        }
        enqueue(finalState, Operation.LifecycleImpact.ADDING, fragmentStateManager)
    }

    fun enqueueShow(fragmentStateManager: FragmentStateManager) {
        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
            Log.v(
                FragmentManager.TAG,
                "SpecialEffectsController: Enqueuing show operation for fragment " +
                    fragmentStateManager.fragment
            )
        }
        enqueue(Operation.State.VISIBLE, Operation.LifecycleImpact.NONE, fragmentStateManager)
    }

    fun enqueueHide(fragmentStateManager: FragmentStateManager) {
        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
            Log.v(
                FragmentManager.TAG,
                "SpecialEffectsController: Enqueuing hide operation for fragment " +
                    fragmentStateManager.fragment
            )
        }
        enqueue(Operation.State.GONE, Operation.LifecycleImpact.NONE, fragmentStateManager)
    }

    fun enqueueRemove(fragmentStateManager: FragmentStateManager) {
        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
            Log.v(
                FragmentManager.TAG,
                "SpecialEffectsController: Enqueuing remove operation for fragment " +
                    fragmentStateManager.fragment
            )
        }
        enqueue(Operation.State.REMOVED, Operation.LifecycleImpact.REMOVING, fragmentStateManager)
    }

    private fun enqueue(
        finalState: Operation.State,
        lifecycleImpact: Operation.LifecycleImpact,
        fragmentStateManager: FragmentStateManager
    ) {
        synchronized(pendingOperations) {
            val signal = CancellationSignal()
            val existingOperation = findPendingOperation(fragmentStateManager.fragment)
            if (existingOperation != null) {
                // Update the existing operation by merging in the new information
                // rather than creating a new Operation entirely
                existingOperation.mergeWith(finalState, lifecycleImpact)
                return
            }
            val operation = FragmentStateManagerOperation(
                finalState, lifecycleImpact, fragmentStateManager, signal
            )
            pendingOperations.add(operation)
            // Ensure that we still run the applyState() call for pending operations
            operation.addCompletionListener {
                if (pendingOperations.contains(operation)) {
                    operation.finalState.applyState(operation.fragment.mView)
                }
            }
            // Ensure that we remove the Operation from the list of
            // operations when the operation is complete
            operation.addCompletionListener {
                pendingOperations.remove(operation)
                runningOperations.remove(operation)
            }
        }
    }

    fun updateOperationDirection(isPop: Boolean) {
        operationDirectionIsPop = isPop
    }

    fun markPostponedState() {
        synchronized(pendingOperations) {
            updateFinalState()
            val lastEnteringFragment = pendingOperations.lastOrNull { operation ->
                // Only consider operations with entering transitions
                val currentState = operation.fragment.mView.asOperationState()
                operation.finalState == Operation.State.VISIBLE &&
                    currentState != Operation.State.VISIBLE
            }?.fragment
            // The container is considered postponed if the Fragment
            // associated with the last entering Operation is postponed
            isContainerPostponed = lastEnteringFragment?.isPostponed ?: false
        }
    }

    fun forcePostponedExecutePendingOperations() {
        if (isContainerPostponed) {
            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                Log.v(
                    FragmentManager.TAG,
                    "SpecialEffectsController: Forcing postponed operations"
                )
            }
            isContainerPostponed = false
            executePendingOperations()
        }
    }

    fun executePendingOperations() {
        if (isContainerPostponed) {
            // No operations should execute while the container is postponed
            return
        }
        // If the container is not attached to the window, ignore the special effect
        // since none of the special effect systems will run them anyway.
        if (!ViewCompat.isAttachedToWindow(container)) {
            forceCompleteAllOperations()
            operationDirectionIsPop = false
            return
        }
        synchronized(pendingOperations) {
            if (pendingOperations.isNotEmpty()) {
                val currentlyRunningOperations = runningOperations.toMutableList()
                runningOperations.clear()
                for (operation in currentlyRunningOperations) {
                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                        Log.v(
                            FragmentManager.TAG,
                            "SpecialEffectsController: Cancelling operation $operation"
                        )
                    }
                    operation.cancel()
                    if (!operation.isComplete) {
                        // Re-add any animations that didn't synchronously call complete()
                        // to continue to track them as running operations
                        runningOperations.add(operation)
                    }
                }
                updateFinalState()
                val newPendingOperations = pendingOperations.toMutableList()
                pendingOperations.clear()
                runningOperations.addAll(newPendingOperations)
                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                    Log.v(
                        FragmentManager.TAG,
                        "SpecialEffectsController: Executing pending operations"
                    )
                }
                for (operation in newPendingOperations) {
                    operation.onStart()
                }
                executeOperations(newPendingOperations, operationDirectionIsPop)
                operationDirectionIsPop = false
                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                    Log.v(
                        FragmentManager.TAG,
                        "SpecialEffectsController: Finished executing pending operations"
                    )
                }
            }
        }
    }

    fun forceCompleteAllOperations() {
        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
            Log.v(
                FragmentManager.TAG,
                "SpecialEffectsController: Forcing all operations to complete"
            )
        }
        val attachedToWindow = ViewCompat.isAttachedToWindow(container)
        synchronized(pendingOperations) {
            updateFinalState()
            for (operation in pendingOperations) {
                operation.onStart()
            }

            // First cancel running operations
            val runningOperations = runningOperations.toMutableList()
            for (operation in runningOperations) {
                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                    val notAttachedMessage = if (attachedToWindow) {
                        ""
                    } else {
                        "Container $container is not attached to window. "
                    }
                    Log.v(
                        FragmentManager.TAG,
                        "SpecialEffectsController: " + notAttachedMessage +
                            "Cancelling running operation $operation"
                    )
                }
                operation.cancel()
            }

            // Then cancel pending operations
            val pendingOperations = pendingOperations.toMutableList()
            for (operation in pendingOperations) {
                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                    val notAttachedMessage = if (attachedToWindow) {
                        ""
                    } else {
                        "Container $container is not attached to window. "
                    }
                    Log.v(
                        FragmentManager.TAG,
                        "SpecialEffectsController: " + notAttachedMessage +
                            "Cancelling pending operation $operation"
                    )
                }
                operation.cancel()
            }
        }
    }

    private fun updateFinalState() {
        for (operation in pendingOperations) {
            // update the final state of adding operations
            if (operation.lifecycleImpact == Operation.LifecycleImpact.ADDING) {
                val fragment = operation.fragment
                val view = fragment.requireView()
                val finalState = Operation.State.from(view.visibility)
                operation.mergeWith(finalState, Operation.LifecycleImpact.NONE)
            }
        }
    }

    /**
     * Execute all of the given operations.
     *
     * If there are no special effects for a given operation, the SpecialEffectsController
     * should call [Operation.complete]. Otherwise, a
     * [CancellationSignal] representing each special effect should be added via
     * [Operation.markStartedSpecialEffect], calling
     * [Operation.completeSpecialEffect] when that specific
     * special effect finishes. When the last started special effect is completed,
     * [Operation.completeSpecialEffect] will call
     * [Operation.complete] automatically.
     *
     * It is **strongly recommended** that each [CancellationSignal] added with
     * [Operation.markStartedSpecialEffect] listen for cancellation,
     * properly cancelling the special effect when the signal is cancelled.
     *
     * @param operations the list of operations to execute in order.
     * @param isPop whether this set of operations should be considered as triggered by a 'pop'.
     * This can be used to control the direction of any special effects if they
     * are not symmetric.
     */
    abstract fun executeOperations(
        operations: List<@JvmSuppressWildcards Operation>,
        isPop: Boolean
    )

    /**
     * Class representing an ongoing special effects operation.
     *
     * @see executeOperations
     */
    internal open class Operation(
        /**
         * The final state after this operation should be.
         */
        var finalState: State,
        /**
         * How this Operation affects the lifecycle of the fragment.
         */
        var lifecycleImpact: LifecycleImpact,
        /**
         * The Fragment being added / removed.
         */
        val fragment: Fragment,
        /**
         * A signal for handling cancellation
         */
        cancellationSignal: CancellationSignal
    ) {
        /**
         * The state that the fragment's View should be in after applying this operation.
         *
         * @see applyState
         */
        internal enum class State {
            /**
             * The fragment's view should be completely removed from the container.
             */
            REMOVED,
            /**
             * The fragment's view should be made [View.VISIBLE].
             */
            VISIBLE,
            /**
             * The fragment's view should be made [View.GONE].
             */
            GONE,
            /**
             * The fragment's view should be made [View.INVISIBLE].
             */
            INVISIBLE;

            /**
             * Applies this state to the given View.
             *
             * @param view The View to apply this state to.
             */
            fun applyState(view: View) {
                when (this) {
                    REMOVED -> {
                        val parent = view.parent as? ViewGroup
                        if (parent != null) {
                            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                                Log.v(
                                    FragmentManager.TAG, "SpecialEffectsController: " +
                                        "Removing view $view from container $parent"
                                )
                            }
                            parent.removeView(view)
                        }
                    }

                    VISIBLE -> {
                        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                            Log.v(
                                FragmentManager.TAG, "SpecialEffectsController: " +
                                    "Setting view $view to VISIBLE"
                            )
                        }
                        view.visibility = View.VISIBLE
                    }

                    GONE -> {
                        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                            Log.v(
                                FragmentManager.TAG,
                                "SpecialEffectsController: Setting view $view to GONE"
                            )
                        }
                        view.visibility = View.GONE
                    }

                    INVISIBLE -> {
                        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                            Log.v(
                                FragmentManager.TAG,
                                "SpecialEffectsController: Setting view $view to INVISIBLE"
                            )
                        }
                        view.visibility = View.INVISIBLE
                    }
                }
            }

            companion object {
                /**
                 * Create a new State from the [view's visibility][View.getVisibility].
                 */
                fun View.asOperationState() = if (alpha == 0f && visibility == View.VISIBLE) {
                    // We should consider views with an alpha of 0 as INVISIBLE.
                    INVISIBLE
                } else {
                    from(visibility)
                }

                /**
                 * Create a new State from the visibility of a View.
                 *
                 * @param visibility The visibility constant to translate into a State.
                 * @return A new State from the visibility.
                 */
                @JvmStatic
                fun from(visibility: Int): State {
                    return when (visibility) {
                        View.VISIBLE -> VISIBLE
                        View.INVISIBLE -> INVISIBLE
                        View.GONE -> GONE
                        else -> throw IllegalArgumentException("Unknown visibility $visibility")
                    }
                }
            }
        }

        /**
         * The impact that this operation has on the lifecycle of the fragment.
         */
        internal enum class LifecycleImpact {
            /**
             * No impact on the fragment's lifecycle.
             */
            NONE,
            /**
             * This operation is associated with adding a fragment.
             */
            ADDING,
            /**
             * This operation is associated with removing a fragment.
             */
            REMOVING,
        }

        private val completionListeners = mutableListOf<Runnable>()
        private val specialEffectsSignals = mutableSetOf<CancellationSignal>()
        var isCanceled = false
            private set
        var isComplete = false
            private set

        init {
            // Connect the CancellationSignal to our own
            cancellationSignal.setOnCancelListener { cancel() }
        }

        override fun toString(): String {
            val identityHash = Integer.toHexString(System.identityHashCode(this))
            return "Operation {$identityHash} {" +
                "finalState = $finalState " +
                "lifecycleImpact = $lifecycleImpact " +
                "fragment = $fragment}"
        }

        fun cancel() {
            if (isCanceled) {
                return
            }
            isCanceled = true
            if (specialEffectsSignals.isEmpty()) {
                complete()
            } else {
                val signals = specialEffectsSignals.toMutableSet()
                for (signal in signals) {
                    signal.cancel()
                }
            }
        }

        fun mergeWith(finalState: State, lifecycleImpact: LifecycleImpact) {
            when (lifecycleImpact) {
                LifecycleImpact.ADDING -> if (this.finalState == State.REMOVED) {
                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                        Log.v(
                            FragmentManager.TAG,
                            "SpecialEffectsController: For fragment $fragment " +
                                "mFinalState = REMOVED -> VISIBLE. " +
                                "mLifecycleImpact = ${this.lifecycleImpact} to ADDING."
                        )
                    }
                    // Applying an ADDING operation to a REMOVED fragment
                    // moves it back to ADDING
                    this.finalState = State.VISIBLE
                    this.lifecycleImpact = LifecycleImpact.ADDING
                }
                LifecycleImpact.REMOVING -> {
                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                        Log.v(
                            FragmentManager.TAG,
                            "SpecialEffectsController: For fragment $fragment " +
                                "mFinalState = ${this.finalState} -> REMOVED. " +
                                "mLifecycleImpact  = ${this.lifecycleImpact} to REMOVING."
                        )
                    }
                    // Any REMOVING operation overrides whatever we had before
                    this.finalState = State.REMOVED
                    this.lifecycleImpact = LifecycleImpact.REMOVING
                }
                LifecycleImpact.NONE -> // This is a hide or show operation
                    if (this.finalState != State.REMOVED) {
                        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                            Log.v(
                                FragmentManager.TAG,
                                "SpecialEffectsController: For fragment $fragment " +
                                    "mFinalState = ${this.finalState} -> $finalState."
                            )
                        }
                        this.finalState = finalState
                    }
            }
        }

        fun addCompletionListener(listener: Runnable) {
            completionListeners.add(listener)
        }

        /**
         * Callback for when the operation is about to start.
         */
        open fun onStart() {}

        /**
         * Add new [CancellationSignal] for special effects.
         *
         * @param signal A CancellationSignal that can be used to cancel this special effect.
         */
        fun markStartedSpecialEffect(signal: CancellationSignal) {
            onStart()
            specialEffectsSignals.add(signal)
        }

        /**
         * Complete a [CancellationSignal] that was previously added with
         * [markStartedSpecialEffect].
         *
         * This calls through to [Operation.complete] when the last special effect is
         * complete.
         */
        fun completeSpecialEffect(signal: CancellationSignal) {
            if (specialEffectsSignals.remove(signal) && specialEffectsSignals.isEmpty()) {
                complete()
            }
        }

        /**
         * Mark this Operation as complete. This should only be called when all
         * special effects associated with this Operation have completed successfully.
         */
        @CallSuper
        open fun complete() {
            if (isComplete) {
                return
            }
            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                Log.v(
                    FragmentManager.TAG,
                    "SpecialEffectsController: $this has called complete."
                )
            }
            isComplete = true
            completionListeners.forEach { listener ->
                listener.run()
            }
        }
    }

    private class FragmentStateManagerOperation(
        finalState: State,
        lifecycleImpact: LifecycleImpact,
        private val fragmentStateManager: FragmentStateManager,
        cancellationSignal: CancellationSignal
    ) : Operation(
        finalState, lifecycleImpact, fragmentStateManager.fragment,
        cancellationSignal
    ) {
        override fun onStart() {
            if (lifecycleImpact == LifecycleImpact.ADDING) {
                val fragment = fragmentStateManager.fragment
                val focusedView = fragment.mView.findFocus()
                if (focusedView != null) {
                    fragment.focusedView = focusedView
                    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                        Log.v(
                            FragmentManager.TAG,
                            "requestFocus: Saved focused view $focusedView for Fragment $fragment"
                        )
                    }
                }
                val view = this.fragment.requireView()
                // We need to ensure that the fragment's view is re-added
                // for ADDING operations to properly handle cases where the
                // exit animation was interrupted.
                if (view.parent == null) {
                    fragmentStateManager.addViewToContainer()
                    view.alpha = 0f
                }
                // Change the view alphas back to their original values before we execute our
                // transitions.
                if (view.alpha == 0f && view.visibility == View.VISIBLE) {
                    view.visibility = View.INVISIBLE
                }
                view.alpha = fragment.postOnViewCreatedAlpha
            } else if (lifecycleImpact == LifecycleImpact.REMOVING) {
                val fragment = fragmentStateManager.fragment
                val view = fragment.requireView()
                if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                    Log.v(
                        FragmentManager.TAG,
                        "Clearing focus ${view.findFocus()} on view $view for Fragment $fragment"
                    )
                }
                view.clearFocus()
            }
        }

        override fun complete() {
            super.complete()
            fragmentStateManager.moveToExpectedState()
        }
    }

    companion object {
        /**
         * Get the [SpecialEffectsController] for a given container if it already exists
         * or create it. This will automatically find the containing FragmentManager and use the
         * factory provided by [FragmentManager.getSpecialEffectsControllerFactory].
         *
         * @param container ViewGroup to find the associated SpecialEffectsController for.
         * @return a SpecialEffectsController for the given container
         */
        @JvmStatic
        fun getOrCreateController(
            container: ViewGroup,
            fragmentManager: FragmentManager
        ): SpecialEffectsController {
            val factory = fragmentManager.specialEffectsControllerFactory
            return getOrCreateController(container, factory)
        }

        /**
         * Get the [SpecialEffectsController] for a given container if it already exists
         * or create it using the given [SpecialEffectsControllerFactory] if it does not.
         *
         * @param container ViewGroup to find the associated SpecialEffectsController for.
         * @param factory The factory to use to create a new SpecialEffectsController if one does
         * not already exist for this container.
         * @return a SpecialEffectsController for the given container
         */
        @JvmStatic
        fun getOrCreateController(
            container: ViewGroup,
            factory: SpecialEffectsControllerFactory
        ): SpecialEffectsController {
            val controller = container.getTag(R.id.special_effects_controller_view_tag)
            if (controller is SpecialEffectsController) {
                return controller
            }
            // Else, create a new SpecialEffectsController
            val newController = factory.createController(container)
            container.setTag(R.id.special_effects_controller_view_tag, newController)
            return newController
        }
    }
}