DynamicIncludeGraphNavigator.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.navigation.dynamicfeatures

import android.content.Context
import android.content.res.Resources
import android.os.Bundle
import android.util.AttributeSet
import androidx.core.content.withStyledAttributes
import androidx.navigation.NavDestination
import androidx.navigation.NavGraph
import androidx.navigation.NavInflater
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.NavigatorProvider
import androidx.navigation.get

/**
 * Navigator for `include-dynamic`.
 *
 * Use it for navigating to NavGraphs contained within a dynamic feature module.
 */
@Navigator.Name("include-dynamic")
class DynamicIncludeGraphNavigator(
    private val context: Context,
    private val navigatorProvider: NavigatorProvider,
    private val navInflater: NavInflater,
    private val installManager: DynamicInstallManager
) : Navigator<DynamicIncludeGraphNavigator.DynamicIncludeNavGraph>() {

    private val createdDestinations = mutableListOf<DynamicIncludeNavGraph>()

    override fun createDestination(): DynamicIncludeNavGraph {
        return DynamicIncludeNavGraph(this).also {
            createdDestinations.add(it)
        }
    }

    /**
     * @throws Resources.NotFoundException if the [destination] does not have a valid
     * `graphResourceName` and `graphPackage`.
     * @throws IllegalStateException if the [destination] does not have a parent.
     */
    override fun navigate(
        destination: DynamicIncludeNavGraph,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ): NavDestination? {
        val extras = navigatorExtras as? DynamicExtras

        val moduleName = destination.moduleName

        return if (moduleName != null && installManager.needsInstall(moduleName)) {
            installManager.performInstall(destination, args, extras, moduleName)
        } else {
            val includedNav = replaceWithIncludedNav(destination)
            val navigator: Navigator<NavDestination> = navigatorProvider[includedNav.navigatorName]
            navigator.navigate(includedNav, args, navOptions, navigatorExtras)
        }
    }

    /**
     * Replace the given [destination] with the included navigation graph it references.
     *
     * @return the newly inflated included navigation graph
     */
    private fun replaceWithIncludedNav(destination: DynamicIncludeNavGraph): NavGraph {
        val graphId = context.resources.getIdentifier(
            destination.graphResourceName, "navigation",
            destination.graphPackage)
        if (graphId == 0) {
            throw Resources.NotFoundException(
                "${destination.graphPackage}:navigation/${destination.graphResourceName}")
        }
        val includedNav = navInflater.inflate(graphId)
        check(!(includedNav.id != 0 && includedNav.id != destination.id)) {
            "The included <navigation>'s id is different from " +
                    "the destination id. Either remove the <navigation> id or make them " +
                    " match."
        }
        includedNav.id = destination.id
        val outerNav = destination.parent
            ?: throw IllegalStateException(
                "The destination ${destination.id} does not have a parent. " +
                        "Make sure it is attached to a NavGraph.")
        // no need to remove previous destination, id is used as key in map
        outerNav.addDestination(includedNav)
        return includedNav
    }

    override fun popBackStack() = true

    override fun onSaveState(): Bundle? {
        // Return a non-null Bundle to get a callback to onRestoreState
        return Bundle.EMPTY
    }

    override fun onRestoreState(savedState: Bundle) {
        super.onRestoreState(savedState)
        val iterator = createdDestinations.iterator()
        while (iterator.hasNext()) {
            val dynamicNavGraph = iterator.next()
            val moduleName = dynamicNavGraph.moduleName
            if (moduleName == null || !installManager.needsInstall(moduleName)) {
                replaceWithIncludedNav(dynamicNavGraph)
            }
            iterator.remove()
        }
    }

    /**
     * The graph for dynamic-include.
     *
     * This class contains information to navigate to a DynamicNavGraph which is contained
     * within a dynamic feature module.
     */
    class DynamicIncludeNavGraph
    internal constructor(navGraphNavigator: Navigator<out NavDestination>) :
        NavDestination(navGraphNavigator) {

        /**
         * Resource name of the graph.
         */
        var graphResourceName: String? = null

        /**
         * The graph's package.
         */
        var graphPackage: String? = null

        /**
         * Name of the module containing the included graph, if set.
         */
        var moduleName: String? = null

        override fun onInflate(context: Context, attrs: AttributeSet) {
            super.onInflate(context, attrs)
            context.withStyledAttributes(attrs, R.styleable.DynamicIncludeGraphNavigator) {
                graphPackage = getString(R.styleable.DynamicIncludeGraphNavigator_graphPackage)
                require(!graphPackage.isNullOrEmpty()) {
                    "graphPackage must be set for dynamic navigation"
                }

                graphResourceName =
                    getString(R.styleable.DynamicIncludeGraphNavigator_graphResName)
                require(!graphPackage.isNullOrEmpty()) {
                    "graphResName must be set for dynamic navigation"
                }

                moduleName = getString(R.styleable.DynamicIncludeGraphNavigator_moduleName)
            }
        }
    }
}