1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.navigation.dynamicfeatures
18 
19 import android.content.Context
20 import android.os.Bundle
21 import android.util.AttributeSet
22 import androidx.annotation.RestrictTo
23 import androidx.core.content.withStyledAttributes
24 import androidx.navigation.NavBackStackEntry
25 import androidx.navigation.NavDestination
26 import androidx.navigation.NavGraph
27 import androidx.navigation.NavGraphNavigator
28 import androidx.navigation.NavOptions
29 import androidx.navigation.Navigator
30 import androidx.navigation.NavigatorProvider
31 
32 /**
33  * Navigator for graphs in dynamic feature modules.
34  *
35  * This class handles navigating to a progress destination when the installation of a dynamic
36  * feature module is required. By default, the progress destination set by
37  * [installDefaultProgressDestination] will be used, but this can be overridden by setting the
38  * `app:progressDestinationId` attribute in your navigation XML file.
39  */
40 @Navigator.Name("navigation")
41 public class DynamicGraphNavigator(
42     private val navigatorProvider: NavigatorProvider,
43     private val installManager: DynamicInstallManager
44 ) : NavGraphNavigator(navigatorProvider) {
45 
46     /** @return The progress destination supplier if any is set. */
47     internal var defaultProgressDestinationSupplier: (() -> NavDestination)? = null
48         private set
49 
50     internal val destinationsWithoutDefaultProgressDestination = mutableListOf<DynamicNavGraph>()
51 
52     /**
53      * Navigate to a destination.
54      *
55      * In case the destination module is installed the navigation will trigger directly. Otherwise
56      * the dynamic feature module is requested and navigation is postponed until the module has
57      * successfully been installed.
58      */
navigatenull59     override fun navigate(
60         entries: List<NavBackStackEntry>,
61         navOptions: NavOptions?,
62         navigatorExtras: Extras?
63     ) {
64         for (entry in entries) {
65             navigate(entry, navOptions, navigatorExtras)
66         }
67     }
68 
navigatenull69     private fun navigate(
70         entry: NavBackStackEntry,
71         navOptions: NavOptions?,
72         navigatorExtras: Extras?
73     ) {
74         val destination = entry.destination
75         val extras = if (navigatorExtras is DynamicExtras) navigatorExtras else null
76         if (destination is DynamicNavGraph) {
77             val moduleName = destination.moduleName
78             if (moduleName != null && installManager.needsInstall(moduleName)) {
79                 installManager.performInstall(entry, extras, moduleName)
80                 return
81             }
82         }
83         super.navigate(
84             listOf(entry),
85             navOptions,
86             if (extras != null) extras.destinationExtras else navigatorExtras
87         )
88     }
89 
90     /**
91      * Create a destination for the [DynamicNavGraph].
92      *
93      * @return The created graph.
94      */
createDestinationnull95     override fun createDestination(): DynamicNavGraph {
96         return DynamicNavGraph(this, navigatorProvider)
97     }
98 
99     /**
100      * Installs the default progress destination to this graph via a lambda. This supplies a
101      * [NavDestination] to use when the actual destination is not installed at navigation time.
102      *
103      * This **must** be called before you call [androidx.navigation.NavController.setGraph] to
104      * ensure that all [DynamicNavGraph] instances have the correct progress destination installed
105      * in [onRestoreState].
106      *
107      * @param progressDestinationSupplier The default progress destination supplier.
108      */
installDefaultProgressDestinationnull109     public fun installDefaultProgressDestination(
110         progressDestinationSupplier: () -> NavDestination
111     ) {
112         this.defaultProgressDestinationSupplier = progressDestinationSupplier
113     }
114 
115     /**
116      * Navigates to a destination after progress is done.
117      *
118      * @return The destination to navigate to if any.
119      */
navigateToProgressDestinationnull120     internal fun navigateToProgressDestination(
121         dynamicNavGraph: DynamicNavGraph,
122         progressArgs: Bundle?
123     ) {
124         var progressDestinationId = dynamicNavGraph.progressDestination
125         if (progressDestinationId == 0) {
126             progressDestinationId = installDefaultProgressDestination(dynamicNavGraph)
127         }
128 
129         val progressDestination =
130             dynamicNavGraph.findNode(progressDestinationId)
131                 ?: throw IllegalStateException(
132                     "The progress destination id must be set and " +
133                         "accessible to the module of this navigator."
134                 )
135         val navigator =
136             navigatorProvider.getNavigator<Navigator<NavDestination>>(
137                 progressDestination.navigatorName
138             )
139         val entry = state.createBackStackEntry(progressDestination, progressArgs)
140         navigator.navigate(listOf(entry), null, null)
141     }
142 
143     /**
144      * Install the default progress destination
145      *
146      * @return The [NavDestination#getId] of the newly added progress destination
147      */
installDefaultProgressDestinationnull148     private fun installDefaultProgressDestination(dynamicNavGraph: DynamicNavGraph): Int {
149         val progressDestinationSupplier = defaultProgressDestinationSupplier
150         checkNotNull(progressDestinationSupplier) {
151             "You must set a default progress destination " +
152                 "using DynamicNavGraphNavigator.installDefaultProgressDestination or " +
153                 "pass in an DynamicInstallMonitor in the DynamicExtras.\n" +
154                 "Alternatively, when using NavHostFragment make sure to swap it with " +
155                 "DynamicNavHostFragment. This will take care of setting the default " +
156                 "progress destination for you."
157         }
158         val progressDestination = progressDestinationSupplier.invoke()
159         dynamicNavGraph.addDestination(progressDestination)
160         dynamicNavGraph.progressDestination = progressDestination.id
161         return progressDestination.id
162     }
163 
onSaveStatenull164     override fun onSaveState(): Bundle? {
165         // Return a non-null Bundle to get a callback to onRestoreState
166         return Bundle.EMPTY
167     }
168 
onRestoreStatenull169     override fun onRestoreState(savedState: Bundle) {
170         super.onRestoreState(savedState)
171         val iterator = destinationsWithoutDefaultProgressDestination.iterator()
172         while (iterator.hasNext()) {
173             val dynamicNavGraph = iterator.next()
174             installDefaultProgressDestination(dynamicNavGraph)
175             iterator.remove()
176         }
177     }
178 
179     /** The [NavGraph] for dynamic features. */
180     public class DynamicNavGraph(
181         @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
182         internal val navGraphNavigator: DynamicGraphNavigator,
183         @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
184         internal val navigatorProvider: NavigatorProvider
185     ) : NavGraph(navGraphNavigator) {
186 
187         internal companion object {
188 
189             /**
190              * Get the [DynamicNavGraph] for a supplied [NavDestination] or throw an exception if
191              * it's not a [DynamicNavGraph].
192              */
getOrThrownull193             internal fun getOrThrow(destination: NavDestination): DynamicNavGraph {
194                 return destination.parent as? DynamicNavGraph
195                     ?: throw IllegalStateException(
196                         "Dynamic destinations must be part of a DynamicNavGraph.\n" +
197                             "You can use DynamicNavHostFragment, which will take care of " +
198                             "setting up the NavController for Dynamic destinations.\n" +
199                             "If you're not using Fragments, you must set up the " +
200                             "NavigatorProvider manually."
201                     )
202             }
203         }
204 
205         /** The dynamic feature's module name. */
206         public var moduleName: String? = null
207 
208         /**
209          * Resource id of progress destination. This will be preferred over any default progress
210          * destination set by [installDefaultProgressDestination].
211          */
212         public var progressDestination: Int = 0
213 
onInflatenull214         override fun onInflate(context: Context, attrs: AttributeSet) {
215             super.onInflate(context, attrs)
216             context.withStyledAttributes(attrs, R.styleable.DynamicGraphNavigator) {
217                 moduleName = getString(R.styleable.DynamicGraphNavigator_moduleName)
218                 progressDestination =
219                     getResourceId(R.styleable.DynamicGraphNavigator_progressDestination, 0)
220                 if (progressDestination == 0) {
221                     navGraphNavigator.destinationsWithoutDefaultProgressDestination.add(
222                         this@DynamicNavGraph
223                     )
224                 }
225             }
226         }
227 
equalsnull228         override fun equals(other: Any?): Boolean {
229             if (this === other) return true
230             if (other == null || other !is DynamicNavGraph) return false
231             return super.equals(other) &&
232                 moduleName == other.moduleName &&
233                 progressDestination == other.progressDestination
234         }
235 
hashCodenull236         override fun hashCode(): Int {
237             var result = super.hashCode()
238             result = 31 * result + moduleName.hashCode()
239             result = 31 * result + progressDestination.hashCode()
240             return result
241         }
242     }
243 }
244