1 /* 2 * Copyright (C) 2017 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 package androidx.navigation 17 18 import android.app.Activity 19 import android.app.PendingIntent 20 import android.content.ComponentName 21 import android.content.Context 22 import android.content.ContextWrapper 23 import android.content.Intent 24 import androidx.annotation.IdRes 25 import androidx.annotation.NavigationRes 26 import androidx.core.app.TaskStackBuilder 27 import androidx.navigation.NavDestination.Companion.createRoute 28 import androidx.navigation.internal.NavContext 29 import androidx.savedstate.SavedState 30 import androidx.savedstate.read 31 32 /** 33 * Class used to construct deep links to a particular destination in a [NavGraph]. 34 * 35 * When this deep link is triggered: 36 * 1. The task is cleared. 37 * 2. The destination and all of its parents will be on the back stack. 38 * 3. Calling [NavController.navigateUp] will navigate to the parent of the destination. 39 * 40 * The parent of the destination is the [start destination][NavGraph.getStartDestination] of the 41 * containing [navigation graph][NavGraph]. In the cases where the destination is the start 42 * destination of its containing navigation graph, the start destination of its grandparent is used. 43 * 44 * You can construct an instance directly with [NavDeepLinkBuilder] or build one using an existing 45 * [NavController] via [NavController.createDeepLink]. 46 * 47 * If the context passed in here is not an [Activity], this method will use 48 * [android.content.pm.PackageManager.getLaunchIntentForPackage] as the default activity to launch, 49 * if available. 50 * 51 * @param context Context used to create deep links 52 * @see NavDeepLinkBuilder.setComponentName 53 */ 54 public class NavDeepLinkBuilder(private val context: Context) { 55 internal val navContext = NavContext(context) 56 57 private class DeepLinkDestination 58 constructor(val destinationId: Int, val arguments: SavedState?) 59 60 private val activity: Activity? = <lambda>null61 generateSequence(context) { (it as? ContextWrapper)?.baseContext } <lambda>null62 .mapNotNull { it as? Activity } 63 .firstOrNull() 64 private val intent: Intent = 65 if (activity != null) { 66 Intent(context, activity.javaClass) 67 } else { 68 val launchIntent = 69 context.packageManager.getLaunchIntentForPackage(context.packageName) 70 launchIntent ?: Intent() 71 } <lambda>null72 .also { it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) } 73 private var graph: NavGraph? = null 74 private val destinations = mutableListOf<DeepLinkDestination>() 75 private var globalArgs: SavedState? = null 76 77 /** @see NavController.createDeepLink */ 78 internal constructor(navController: NavController) : this(navController.context) { 79 graph = navController.graph 80 } 81 82 /** 83 * Sets an explicit Activity to be started by the deep link created by this class. 84 * 85 * @param activityClass The Activity to start. This Activity should have a [NavController] which 86 * uses the same [NavGraph] used to construct this deep link. 87 * @return this object for chaining 88 */ setComponentNamenull89 public fun setComponentName(activityClass: Class<out Activity?>): NavDeepLinkBuilder { 90 return setComponentName(ComponentName(context, activityClass)) 91 } 92 93 /** 94 * Sets an explicit Activity to be started by the deep link created by this class. 95 * 96 * @param componentName The Activity to start. This Activity should have a [NavController] which 97 * uses the same [NavGraph] used to construct this deep link. 98 * @return this object for chaining 99 */ setComponentNamenull100 public fun setComponentName(componentName: ComponentName): NavDeepLinkBuilder { 101 intent.component = componentName 102 return this 103 } 104 105 /** 106 * Sets the graph that contains the [deep link destination][setDestination]. 107 * 108 * @param navGraphId ID of the [NavGraph] containing the deep link destination 109 * @return this object for chaining 110 */ setGraphnull111 public fun setGraph(@NavigationRes navGraphId: Int): NavDeepLinkBuilder { 112 return setGraph(NavInflater(context, PermissiveNavigatorProvider()).inflate(navGraphId)) 113 } 114 115 /** 116 * Sets the graph that contains the [deep link destination][setDestination]. 117 * 118 * If you do not have access to a [NavController], you can create a [NavigatorProvider] and use 119 * that to programmatically construct a navigation graph or use [NavInflater][NavInflater]. 120 * 121 * @param navGraph The [NavGraph] containing the deep link destination 122 * @return this object for chaining 123 */ setGraphnull124 public fun setGraph(navGraph: NavGraph): NavDeepLinkBuilder { 125 graph = navGraph 126 verifyAllDestinations() 127 return this 128 } 129 130 /** 131 * Sets the destination id to deep link to. Any destinations previous added via [addDestination] 132 * are cleared, effectively resetting this object back to only this single destination. 133 * 134 * @param destId destination ID to deep link to. 135 * @param args Arguments to pass to this destination and any synthetic back stack created due to 136 * this destination being added. 137 * @return this object for chaining 138 */ 139 @JvmOverloads setDestinationnull140 public fun setDestination(@IdRes destId: Int, args: SavedState? = null): NavDeepLinkBuilder { 141 destinations.clear() 142 destinations.add(DeepLinkDestination(destId, args)) 143 if (graph != null) { 144 verifyAllDestinations() 145 } 146 return this 147 } 148 149 /** 150 * Sets the destination route to deep link to. Any destinations previous added via 151 * [.addDestination] are cleared, effectively resetting this object back to only this single 152 * destination. 153 * 154 * @param destRoute destination route to deep link to. 155 * @param args Arguments to pass to this destination and any synthetic back stack created due to 156 * this destination being added. 157 * @return this object for chaining 158 */ 159 @JvmOverloads setDestinationnull160 public fun setDestination(destRoute: String, args: SavedState? = null): NavDeepLinkBuilder { 161 destinations.clear() 162 destinations.add(DeepLinkDestination(createRoute(destRoute).hashCode(), args)) 163 if (graph != null) { 164 verifyAllDestinations() 165 } 166 return this 167 } 168 169 /** 170 * Add a new destination id to deep link to. This builds off any previous calls to this method 171 * or calls to [setDestination], building the minimal synthetic back stack of start destinations 172 * between the previous deep link destination and the newly added deep link destination. 173 * 174 * This means that if R.navigation.nav_graph has startDestination= R.id.start_destination, 175 * ``` 176 * navDeepLinkBuilder 177 * .setGraph(R.navigation.nav_graph) 178 * .addDestination(R.id.second_destination, null) 179 * ``` 180 * 181 * is equivalent to 182 * 183 * ``` 184 * navDeepLinkBuilder 185 * .setGraph(R.navigation.nav_graph) 186 * .addDestination(R.id.start_destination, null) 187 * .addDestination(R.id.second_destination, null) 188 * ``` 189 * 190 * Use the second form to assign specific arguments to the start destination. 191 * 192 * @param destId destination ID to deep link to. 193 * @param args Arguments to pass to this destination and any synthetic back stack created due to 194 * this destination being added. 195 * @return this object for chaining 196 */ 197 @JvmOverloads addDestinationnull198 public fun addDestination(@IdRes destId: Int, args: SavedState? = null): NavDeepLinkBuilder { 199 destinations.add(DeepLinkDestination(destId, args)) 200 if (graph != null) { 201 verifyAllDestinations() 202 } 203 return this 204 } 205 206 /** 207 * Add a new destination route to deep link to. This builds off any previous calls to this 208 * method or calls to [.setDestination], building the minimal synthetic back stack of start 209 * destinations between the previous deep link destination and the newly added deep link 210 * destination. 211 * 212 * @param route destination route to deep link to. 213 * @param args Arguments to pass to this destination and any synthetic back stack created due to 214 * this destination being added. 215 * @return this object for chaining 216 */ 217 @JvmOverloads addDestinationnull218 public fun addDestination(route: String, args: SavedState? = null): NavDeepLinkBuilder { 219 destinations.add(DeepLinkDestination(createRoute(route).hashCode(), args)) 220 if (graph != null) { 221 verifyAllDestinations() 222 } 223 return this 224 } 225 findDestinationnull226 private fun findDestination(@IdRes destId: Int): NavDestination? { 227 val possibleDestinations = ArrayDeque<NavDestination>() 228 possibleDestinations.add(graph!!) 229 while (!possibleDestinations.isEmpty()) { 230 val destination = possibleDestinations.removeFirst() 231 if (destination.id == destId) { 232 return destination 233 } else if (destination is NavGraph) { 234 for (child in destination) { 235 possibleDestinations.add(child) 236 } 237 } 238 } 239 return null 240 } 241 verifyAllDestinationsnull242 private fun verifyAllDestinations() { 243 for (destination in destinations) { 244 val destId = destination.destinationId 245 val node = findDestination(destId) 246 if (node == null) { 247 val dest = NavDestination.getDisplayName(navContext, destId) 248 throw IllegalArgumentException( 249 "Navigation destination $dest cannot be found in the navigation graph $graph" 250 ) 251 } 252 } 253 } 254 fillInIntentnull255 private fun fillInIntent() { 256 val deepLinkIds = mutableListOf<Int>() 257 val deepLinkArgs = ArrayList<SavedState?>() 258 var previousDestination: NavDestination? = null 259 for (destination in destinations) { 260 val destId = destination.destinationId 261 val arguments = destination.arguments 262 val node = findDestination(destId) 263 if (node == null) { 264 val dest = NavDestination.getDisplayName(navContext, destId) 265 throw IllegalArgumentException( 266 "Navigation destination $dest cannot be found in the navigation graph $graph" 267 ) 268 } 269 for (id in node.buildDeepLinkIds(previousDestination)) { 270 deepLinkIds.add(id) 271 deepLinkArgs.add(arguments) 272 } 273 previousDestination = node 274 } 275 val idArray = deepLinkIds.toIntArray() 276 intent.putExtra(NavController.KEY_DEEP_LINK_IDS, idArray) 277 intent.putParcelableArrayListExtra(NavController.KEY_DEEP_LINK_ARGS, deepLinkArgs) 278 } 279 280 /** 281 * Set optional arguments to send onto every destination created by this deep link. 282 * 283 * @param args arguments to pass to each destination 284 * @return this object for chaining 285 */ setArgumentsnull286 public fun setArguments(args: SavedState?): NavDeepLinkBuilder { 287 globalArgs = args 288 intent.putExtra(NavController.KEY_DEEP_LINK_EXTRAS, args) 289 return this 290 } 291 292 /** 293 * Construct the full [task stack][TaskStackBuilder] needed to deep link to the given 294 * destination. 295 * 296 * You must have [set a NavGraph][setGraph] and [set a destination][setDestination] before 297 * calling this method. 298 * 299 * @return a [TaskStackBuilder] which can be used to 300 * [send the deep link][TaskStackBuilder.startActivities] or 301 * [create a PendingIntent][TaskStackBuilder.getPendingIntent] to deep link to the given 302 * destination. 303 */ createTaskStackBuildernull304 public fun createTaskStackBuilder(): TaskStackBuilder { 305 checkNotNull(graph) { "You must call setGraph() before constructing the deep link" } 306 check(destinations.isNotEmpty()) { 307 "You must call setDestination() or addDestination() before constructing the deep link" 308 } 309 fillInIntent() 310 // We create a copy of the Intent to ensure the Intent does not have itself 311 // as an extra. This also prevents developers from modifying the internal Intent 312 // via taskStackBuilder.editIntentAt() 313 val taskStackBuilder = 314 TaskStackBuilder.create(context).addNextIntentWithParentStack(Intent(intent)) 315 for (index in 0 until taskStackBuilder.intentCount) { 316 // Attach the original Intent to each Activity so that they can know 317 // they were constructed in response to a deep link 318 taskStackBuilder 319 .editIntentAt(index) 320 ?.putExtra(NavController.KEY_DEEP_LINK_INTENT, intent) 321 } 322 return taskStackBuilder 323 } 324 325 /** 326 * Construct a [PendingIntent] to the [deep link destination][setDestination]. 327 * 328 * This constructs the entire [task stack][createTaskStackBuilder] needed. 329 * 330 * You must have [set a NavGraph][setGraph] and [set a destination][setDestination] before 331 * calling this method. 332 * 333 * @return a PendingIntent constructed with [TaskStackBuilder.getPendingIntent] to deep link to 334 * the given destination 335 */ 336 @Suppress("DEPRECATION") createPendingIntentnull337 public fun createPendingIntent(): PendingIntent { 338 var requestCode = globalArgs?.read { contentDeepHashCode() } ?: 0 339 for (destination in destinations) { 340 val destId = destination.destinationId 341 requestCode = 31 * requestCode + destId 342 val argumentsHashCode = destination.arguments?.read { contentDeepHashCode() } 343 if (argumentsHashCode != null) { 344 requestCode = 31 * requestCode + argumentsHashCode 345 } 346 } 347 return createTaskStackBuilder() 348 .getPendingIntent( 349 requestCode, 350 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 351 )!! 352 } 353 354 /** 355 * A [NavigatorProvider] that only parses the basics: [navigation graphs][NavGraph] and 356 * [destinations][NavDestination], effectively only getting the base destination information. 357 */ 358 private class PermissiveNavigatorProvider : NavigatorProvider() { 359 /** A Navigator that only parses the [NavDestination] attributes. */ 360 private val mDestNavigator: Navigator<NavDestination> = 361 object : Navigator<NavDestination>() { createDestinationnull362 override fun createDestination(): NavDestination { 363 return NavDestination("permissive") 364 } 365 navigatenull366 override fun navigate( 367 destination: NavDestination, 368 args: SavedState?, 369 navOptions: NavOptions?, 370 navigatorExtras: Extras? 371 ): NavDestination? { 372 throw IllegalStateException("navigate is not supported") 373 } 374 popBackStacknull375 override fun popBackStack(): Boolean { 376 throw IllegalStateException("popBackStack is not supported") 377 } 378 } 379 380 @Suppress("UNCHECKED_CAST") getNavigatornull381 override fun <T : Navigator<out NavDestination>> getNavigator(name: String): T { 382 return try { 383 super.getNavigator(name) 384 } catch (e: IllegalStateException) { 385 mDestNavigator as T 386 } 387 } 388 389 init { 390 addNavigator(NavGraphNavigator(this)) 391 } 392 } 393 } 394