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