DynamicGraphNavigator.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.os.Bundle
import android.util.AttributeSet
import androidx.annotation.RestrictTo
import androidx.core.content.withStyledAttributes
import androidx.navigation.NavDestination
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphNavigator
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.NavigatorProvider

/**
 * Navigator for graphs in dynamic feature modules.
 *
 * This class handles navigating to a progress destination when the installation
 * of a dynamic feature module is required. By default, the progress destination set
 * by [installDefaultProgressDestination] will be used, but this can be overridden
 * by setting the `app:progressDestinationId` attribute in your navigation XML file.
 */
@Navigator.Name("navigation")
class DynamicGraphNavigator(
    private val navigatorProvider: NavigatorProvider,
    private val installManager: DynamicInstallManager
) : NavGraphNavigator(navigatorProvider) {

    /**
     * @return The progress destination supplier if any is set.
     */
    internal var defaultProgressDestinationSupplier: (() -> NavDestination)? = null
        private set

    internal val destinationsWithoutDefaultProgressDestination = mutableListOf<DynamicNavGraph>()

    /**
     * Navigate to a destination.
     *
     * In case the destination module is installed the navigation will trigger directly.
     * Otherwise the dynamic feature module is requested and navigation is postponed until the
     * module has successfully been installed.
     */
    override fun navigate(
        destination: NavGraph,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ): NavDestination? {
        val extras = if (navigatorExtras is DynamicExtras) navigatorExtras else null
        if (destination is DynamicNavGraph) {
            val moduleName = destination.moduleName
            if (moduleName != null && installManager.needsInstall(moduleName)) {
                return installManager.performInstall(destination, args, extras, moduleName)
            }
        }
        return super.navigate(
            destination, args, navOptions,
            if (extras != null) extras.destinationExtras else navigatorExtras
        )
    }

    /**
     * Create a destination for the [DynamicNavGraph].
     *
     * @return The created graph.
     */
    override fun createDestination(): DynamicNavGraph {
        return DynamicNavGraph(this, navigatorProvider)
    }

    /**
     * Installs the default progress destination to this graph via a lambda.
     * This supplies a [NavDestination] to use when the actual destination is not installed at
     * navigation time.
     *
     * This **must** be called before you call [androidx.navigation.NavController.setGraph] to
     * ensure that all [DynamicNavGraph] instances have the correct progress destination
     * installed in [onRestoreState].
     *
     * @param progressDestinationSupplier The default progress destination supplier.
     */
    fun installDefaultProgressDestination(
        progressDestinationSupplier: () -> NavDestination
    ) {
        this.defaultProgressDestinationSupplier = progressDestinationSupplier
    }

    /**
     * Navigates to a destination after progress is done.
     *
     * @return The destination to navigate to if any.
     */
    internal fun navigateToProgressDestination(
        dynamicNavGraph: DynamicNavGraph,
        progressArgs: Bundle?
    ): NavDestination? {
        var progressDestinationId = dynamicNavGraph.progressDestination
        if (progressDestinationId == 0) {
            progressDestinationId = installDefaultProgressDestination(dynamicNavGraph)
        }

        val progressDestination = dynamicNavGraph.findNode(progressDestinationId)
            ?: throw IllegalStateException("The progress destination id must be set and " +
                    "accessible to the module of this navigator.")
        val navigator = navigatorProvider.getNavigator<Navigator<NavDestination>>(
            progressDestination.navigatorName
        )
        return navigator.navigate(progressDestination, progressArgs, null, null)
    }

    /**
     * Install the default progress destination
     *
     * @return The [NavDestination.getId] of the newly added progress destination
     */
    private fun installDefaultProgressDestination(dynamicNavGraph: DynamicNavGraph): Int {
        val progressDestinationSupplier = defaultProgressDestinationSupplier
        checkNotNull(progressDestinationSupplier) {
            "You must set a default progress destination " +
                    "using DynamicNavGraphNavigator.installDefaultProgressDestination or " +
                    "pass in an DynamicInstallMonitor in the DynamicExtras.\n" +
                    "Alternatively, when using NavHostFragment make sure to swap it with " +
                    "DynamicNavHostFragment. This will take care of setting the default " +
                    "progress destination for you."
        }
        val progressDestination = progressDestinationSupplier.invoke()
        dynamicNavGraph.addDestination(progressDestination)
        dynamicNavGraph.progressDestination = progressDestination.id
        return progressDestination.id
    }

    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 = destinationsWithoutDefaultProgressDestination.iterator()
        while (iterator.hasNext()) {
            val dynamicNavGraph = iterator.next()
            installDefaultProgressDestination(dynamicNavGraph)
            iterator.remove()
        }
    }

    /**
     * The [NavGraph] for dynamic features.
     */
    class DynamicNavGraph(
        /**
         * @hide
         */
        @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        internal val navGraphNavigator: DynamicGraphNavigator,
        /**
         * @hide
         */
        @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        internal val navigatorProvider: NavigatorProvider
    ) : NavGraph(navGraphNavigator) {

        internal companion object {

            /**
             * Get the [DynamicNavGraph] for a supplied [NavDestination] or throw an
             * exception if it's not a [DynamicNavGraph].
             */
            internal fun getOrThrow(destination: NavDestination): DynamicNavGraph {
                return destination.parent as? DynamicNavGraph
                    ?: throw IllegalStateException(
                        "Dynamic destinations must be part of a DynamicNavGraph.\n" +
                                "You can use DynamicNavHostFragment, which will take care of " +
                                "setting up the NavController for Dynamic destinations.\n" +
                                "If you're not using Fragments, you must set up the " +
                                "NavigatorProvider manually."
                    )
            }
        }

        /**
         * The dynamic feature's module name.
         */
        var moduleName: String? = null

        /**
         * Resource id of progress destination. This will be preferred over any
         * default progress destination set by [installDefaultProgressDestination].
         */
        var progressDestination: Int = 0

        override fun onInflate(context: Context, attrs: AttributeSet) {
            super.onInflate(context, attrs)
            context.withStyledAttributes(attrs, R.styleable.DynamicGraphNavigator) {
                moduleName = getString(R.styleable.DynamicGraphNavigator_moduleName)
                progressDestination = getResourceId(
                    R.styleable.DynamicGraphNavigator_progressDestination, 0)
                if (progressDestination == 0) {
                    navGraphNavigator.destinationsWithoutDefaultProgressDestination
                        .add(this@DynamicNavGraph)
                }
            }
        }
    }
}