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.content.res.Resources
21 import android.os.Bundle
22 import android.util.AttributeSet
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.NavInflater
28 import androidx.navigation.NavInflater.Companion.APPLICATION_ID_PLACEHOLDER
29 import androidx.navigation.NavOptions
30 import androidx.navigation.Navigator
31 import androidx.navigation.NavigatorProvider
32 import androidx.navigation.get
33 
34 /**
35  * Navigator for `include-dynamic`.
36  *
37  * Use it for navigating to NavGraphs contained within a dynamic feature module.
38  */
39 @Navigator.Name("include-dynamic")
40 public class DynamicIncludeGraphNavigator(
41     private val context: Context,
42     private val navigatorProvider: NavigatorProvider,
43     private val navInflater: NavInflater,
44     private val installManager: DynamicInstallManager
45 ) : Navigator<DynamicIncludeGraphNavigator.DynamicIncludeNavGraph>() {
46 
47     internal val packageName: String = context.packageName
48 
49     private val createdDestinations = mutableListOf<DynamicIncludeNavGraph>()
50 
createDestinationnull51     override fun createDestination(): DynamicIncludeNavGraph {
52         return DynamicIncludeNavGraph(this).also { createdDestinations.add(it) }
53     }
54 
55     /**
56      * Navigates to a dynamically included graph from a `com.android.dynamic-feature` module.
57      *
58      * @param entries destination(s) to navigate to
59      * @param navOptions additional options for navigation
60      * @param navigatorExtras extras unique to your Navigator.
61      * @throws Resources.NotFoundException if one of the [entries] does not have a valid
62      *   `graphResourceName` and `graphPackage`.
63      * @throws IllegalStateException if one of the [entries] does not have a parent.
64      * @see Navigator.navigate
65      */
navigatenull66     override fun navigate(
67         entries: List<NavBackStackEntry>,
68         navOptions: NavOptions?,
69         navigatorExtras: Extras?
70     ) {
71         for (entry in entries) {
72             navigate(entry, navOptions, navigatorExtras)
73         }
74     }
75 
76     /**
77      * @throws Resources.NotFoundException if the [entry] does not have a valid `graphResourceName`
78      *   and `graphPackage`.
79      * @throws IllegalStateException if the [entry] does not have a parent.
80      */
navigatenull81     private fun navigate(
82         entry: NavBackStackEntry,
83         navOptions: NavOptions?,
84         navigatorExtras: Extras?
85     ) {
86         val destination = entry.destination as DynamicIncludeNavGraph
87         val extras = navigatorExtras as? DynamicExtras
88 
89         val moduleName = destination.moduleName
90 
91         if (moduleName != null && installManager.needsInstall(moduleName)) {
92             installManager.performInstall(entry, extras, moduleName)
93         } else {
94             val includedNav = replaceWithIncludedNav(destination)
95             val navigator: Navigator<NavDestination> = navigatorProvider[includedNav.navigatorName]
96             val newGraphEntry = state.createBackStackEntry(includedNav, entry.arguments)
97             navigator.navigate(listOf(newGraphEntry), navOptions, navigatorExtras)
98         }
99     }
100 
101     /**
102      * Replace the given [destination] with the included navigation graph it references.
103      *
104      * @return the newly inflated included navigation graph
105      */
replaceWithIncludedNavnull106     private fun replaceWithIncludedNav(destination: DynamicIncludeNavGraph): NavGraph {
107         val graphId =
108             context.resources.getIdentifier(
109                 destination.graphResourceName,
110                 "navigation",
111                 destination.graphPackage
112             )
113         if (graphId == 0) {
114             throw Resources.NotFoundException(
115                 "${destination.graphPackage}:navigation/${destination.graphResourceName}"
116             )
117         }
118         val includedNav = navInflater.inflate(graphId)
119         check(!(includedNav.id != 0 && includedNav.id != destination.id)) {
120             "The included <navigation>'s id ${includedNav.displayName} is different from " +
121                 "the destination id ${destination.displayName}. Either remove the " +
122                 "<navigation> id or make them match."
123         }
124         includedNav.id = destination.id
125         val outerNav =
126             destination.parent
127                 ?: throw IllegalStateException(
128                     "The include-dynamic destination with id ${destination.displayName} " +
129                         "does not have a parent. Make sure it is attached to a NavGraph."
130                 )
131         outerNav.addDestination(includedNav)
132         // Avoid calling replaceWithIncludedNav() on the same destination more than once
133         createdDestinations.remove(destination)
134         return includedNav
135     }
136 
onSaveStatenull137     override fun onSaveState(): Bundle? {
138         // Return a non-null Bundle to get a callback to onRestoreState
139         return Bundle.EMPTY
140     }
141 
onRestoreStatenull142     override fun onRestoreState(savedState: Bundle) {
143         super.onRestoreState(savedState)
144         // replaceWithIncludedNav() can add more elements while we're iterating
145         // through the list so we need to keep iterating until there's no more
146         // unexpanded graphs
147         while (createdDestinations.isNotEmpty()) {
148             // Iterate through a copy to prevent ConcurrentModificationExceptions
149             val iterator = ArrayList(createdDestinations).iterator()
150             // And clear the original list so that the list only contains
151             // newly inflated destinations from the replaceWithIncludedNav() calls
152             // the next time our loop completes
153             createdDestinations.clear()
154             while (iterator.hasNext()) {
155                 val dynamicNavGraph = iterator.next()
156                 val moduleName = dynamicNavGraph.moduleName
157                 if (moduleName == null || !installManager.needsInstall(moduleName)) {
158                     replaceWithIncludedNav(dynamicNavGraph)
159                 }
160             }
161         }
162     }
163 
164     /**
165      * The graph for dynamic-include.
166      *
167      * This class contains information to navigate to a DynamicNavGraph which is contained within a
168      * dynamic feature module.
169      */
170     public class DynamicIncludeNavGraph
171     internal constructor(navGraphNavigator: Navigator<out NavDestination>) :
172         NavDestination(navGraphNavigator) {
173 
174         /** Resource name of the graph. */
175         public var graphResourceName: String? = null
176 
177         /** The graph's package. */
178         public var graphPackage: String? = null
179 
180         /** Name of the module containing the included graph, if set. */
181         public var moduleName: String? = null
182 
onInflatenull183         override fun onInflate(context: Context, attrs: AttributeSet) {
184             super.onInflate(context, attrs)
185             context.withStyledAttributes(attrs, R.styleable.DynamicIncludeGraphNavigator) {
186                 moduleName = getString(R.styleable.DynamicIncludeGraphNavigator_moduleName)
187                 require(!moduleName.isNullOrEmpty()) {
188                     "`moduleName` must be set for <include-dynamic>"
189                 }
190 
191                 graphPackage =
192                     getString(R.styleable.DynamicIncludeGraphNavigator_graphPackage).let {
193                         if (it != null) {
194                             require(it.isNotEmpty()) {
195                                 "`graphPackage` cannot be empty for <include-dynamic>. You can " +
196                                     "omit the `graphPackage` attribute entirely to use the " +
197                                     "default of ${context.packageName}.$moduleName."
198                             }
199                         }
200                         getPackageOrDefault(context, it)
201                     }
202 
203                 graphResourceName = getString(R.styleable.DynamicIncludeGraphNavigator_graphResName)
204                 require(!graphResourceName.isNullOrEmpty()) {
205                     "`graphResName` must be set for <include-dynamic>"
206                 }
207             }
208         }
209 
getPackageOrDefaultnull210         internal fun getPackageOrDefault(context: Context, graphPackage: String?): String {
211             return graphPackage?.replace(APPLICATION_ID_PLACEHOLDER, context.packageName)
212                 ?: "${context.packageName}.$moduleName"
213         }
214 
equalsnull215         override fun equals(other: Any?): Boolean {
216             if (this === other) return true
217             if (other == null || other !is DynamicIncludeNavGraph) return false
218             return super.equals(other) &&
219                 graphResourceName == other.graphResourceName &&
220                 graphPackage == other.graphPackage &&
221                 moduleName == other.moduleName
222         }
223 
hashCodenull224         override fun hashCode(): Int {
225             var result = super.hashCode()
226             result = 31 * result + graphResourceName.hashCode()
227             result = 31 * result + graphPackage.hashCode()
228             result = 31 * result + moduleName.hashCode()
229             return result
230         }
231     }
232 }
233