NavigatorState.kt

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.navigation

import android.os.Bundle
import androidx.annotation.RestrictTo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

/**
 * The NavigatorState encapsulates the state shared between the [Navigator] and the
 * [NavController].
 */
public abstract class NavigatorState {
    private val backStackLock = ReentrantLock(true)
    private val _backStack: MutableStateFlow<List<NavBackStackEntry>> = MutableStateFlow(listOf())
    private val _transitionsInProgress: MutableStateFlow<Set<NavBackStackEntry>> =
        MutableStateFlow(setOf())

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public var isNavigating = false

    /**
     * While the [NavController] is responsible for the combined back stack across all
     * Navigators, this back stack is specifically the set of destinations associated
     * with this Navigator.
     *
     * Changing the back stack must be done via [push] and [pop].
     */
    public val backStack: StateFlow<List<NavBackStackEntry>> = _backStack.asStateFlow()

    /**
     * This is the set of currently running transitions. Use this set to retrieve the entry and call
     * [markTransitionComplete] once the transition is complete.
     */
    public val transitionsInProgress: StateFlow<Set<NavBackStackEntry>> =
        _transitionsInProgress.asStateFlow()

    /**
     * Adds the given [backStackEntry] to the [backStack].
     */
    public open fun push(backStackEntry: NavBackStackEntry) {
        backStackLock.withLock {
            _backStack.value = _backStack.value + backStackEntry
        }
    }

    /**
     * Adds the given [backStackEntry] to the [backStack]. This also adds the given and
     * previous entry to the [set of in progress transitions][transitionsInProgress].
     * Added entries have their [Lifecycle] capped at [Lifecycle.State.STARTED] until an entry is
     * passed into the [markTransitionComplete] callback, when they are allowed to go to
     * [Lifecycle.State.RESUMED].
     *
     * @see transitionsInProgress
     * @see markTransitionComplete
     * @see popWithTransition
     */
    public open fun pushWithTransition(backStackEntry: NavBackStackEntry) {
        val previousEntry = backStack.value.lastOrNull()
        // When navigating, we need to mark the outgoing entry as transitioning until it
        // finishes its outgoing animation.
        if (previousEntry != null) {
            _transitionsInProgress.value = _transitionsInProgress.value + previousEntry
        }
        _transitionsInProgress.value = _transitionsInProgress.value + backStackEntry
        push(backStackEntry)
    }

    /**
     * Create a new [NavBackStackEntry] from a given [destination] and [arguments].
     */
    public abstract fun createBackStackEntry(
        destination: NavDestination,
        arguments: Bundle?
    ): NavBackStackEntry

    /**
     * Pop all destinations up to and including [popUpTo]. This will remove those
     * destinations from the [backStack], saving their state if [saveState] is `true`.
     */
    public open fun pop(popUpTo: NavBackStackEntry, saveState: Boolean) {
        backStackLock.withLock {
            _backStack.value = _backStack.value.takeWhile { it != popUpTo }
        }
    }

    /**
     * Pops all destinations up to and including [popUpTo]. This also adds the given and
     * incoming entry to the [set of in progress transitions][transitionsInProgress]. Added
     * entries have their [Lifecycle] held at [Lifecycle.State.CREATED] until an entry is
     * passed into the [markTransitionComplete] callback, when they are allowed to go to
     * [Lifecycle.State.DESTROYED] and have their state cleared.
     *
     * This will remove those destinations from the [backStack], saving their state if
     * [saveState] is `true`.
     *
     * @see transitionsInProgress
     * @see markTransitionComplete
     * @see pushWithTransition
     */
    public open fun popWithTransition(popUpTo: NavBackStackEntry, saveState: Boolean) {
        _transitionsInProgress.value = _transitionsInProgress.value + popUpTo
        val incomingEntry = backStack.value.lastOrNull { entry ->
            entry != popUpTo &&
                backStack.value.lastIndexOf(entry) < backStack.value.lastIndexOf(popUpTo)
        }
        // When popping, we need to mark the incoming entry as transitioning so we keep it
        // STARTED until the transition completes at which point we can move it to RESUMED
        if (incomingEntry != null) {
            _transitionsInProgress.value = _transitionsInProgress.value + incomingEntry
        }
        pop(popUpTo, saveState)
    }

    /**
     * This removes the given [NavBackStackEntry] from the [set of the transitions in
     * progress][transitionsInProgress]. This should be called in conjunction with
     * [pushWithTransition] and [popWithTransition] as those call are responsible for adding
     * entries to [transitionsInProgress].
     *
     * Failing to call this method could result in entries being prevented from reaching their
     * final [Lifecycle.State]}.
     *
     * @see pushWithTransition
     * @see popWithTransition
     */
    public open fun markTransitionComplete(entry: NavBackStackEntry) {
        _transitionsInProgress.value = _transitionsInProgress.value - entry
    }
}