NavDeepLinkBuilder.kt
/*
* Copyright (C) 2017 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
import android.app.Activity
import android.content.Intent
import android.content.ComponentName
import android.app.PendingIntent
import android.content.Context
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.annotation.NavigationRes
import androidx.core.app.TaskStackBuilder
import androidx.navigation.NavDestination.Companion.createRoute
/**
* Class used to construct deep links to a particular destination in a [NavGraph].
*
* When this deep link is triggered:
*
* 1. The task is cleared.
* 2. The destination and all of its parents will be on the back stack.
* 3. Calling [NavController.navigateUp] will navigate to the parent of the
* destination.
*
* The parent of the destination is the [start destination][NavGraph.getStartDestination]
* of the containing [navigation graph][NavGraph]. In the cases where the destination is
* the start destination of its containing navigation graph, the start destination of its
* grandparent is used.
*
* You can construct an instance directly with [NavDeepLinkBuilder] or build one
* using an existing [NavController] via [NavController.createDeepLink].
*
* If the context passed in here is not an [Activity], this method will use
* [android.content.pm.PackageManager.getLaunchIntentForPackage] as the
* default activity to launch, if available.
*
* @param context Context used to create deep links
* @see NavDeepLinkBuilder.setComponentName
*/
public class NavDeepLinkBuilder(private val context: Context) {
private class DeepLinkDestination constructor(
val destinationId: Int,
val arguments: Bundle?
)
private val intent: Intent = if (context is Activity) {
Intent(context, context.javaClass)
} else {
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
launchIntent ?: Intent()
}.also {
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
private var graph: NavGraph? = null
private val destinations = mutableListOf<DeepLinkDestination>()
private var globalArgs: Bundle? = null
/**
* @see NavController.createDeepLink
*/
internal constructor(navController: NavController) : this(navController.context) {
graph = navController.graph
}
/**
* Sets an explicit Activity to be started by the deep link created by this class.
*
* @param activityClass The Activity to start. This Activity should have a [NavController]
* which uses the same [NavGraph] used to construct this
* deep link.
* @return this object for chaining
*/
public fun setComponentName(activityClass: Class<out Activity?>): NavDeepLinkBuilder {
return setComponentName(ComponentName(context, activityClass))
}
/**
* Sets an explicit Activity to be started by the deep link created by this class.
*
* @param componentName The Activity to start. This Activity should have a [NavController]
* which uses the same [NavGraph] used to construct this
* deep link.
* @return this object for chaining
*/
public fun setComponentName(componentName: ComponentName): NavDeepLinkBuilder {
intent.component = componentName
return this
}
/**
* Sets the graph that contains the [deep link destination][setDestination].
*
* @param navGraphId ID of the [NavGraph] containing the deep link destination
* @return this object for chaining
*/
public fun setGraph(@NavigationRes navGraphId: Int): NavDeepLinkBuilder {
return setGraph(NavInflater(context, PermissiveNavigatorProvider()).inflate(navGraphId))
}
/**
* Sets the graph that contains the [deep link destination][setDestination].
*
* If you do not have access to a [NavController], you can create a
* [NavigatorProvider] and use that to programmatically construct a navigation
* graph or use [NavInflater][NavInflater].
*
* @param navGraph The [NavGraph] containing the deep link destination
* @return this object for chaining
*/
public fun setGraph(navGraph: NavGraph): NavDeepLinkBuilder {
graph = navGraph
verifyAllDestinations()
return this
}
/**
* Sets the destination id to deep link to. Any destinations previous added via
* [addDestination] are cleared, effectively resetting this object
* back to only this single destination.
*
* @param destId destination ID to deep link to.
* @param args Arguments to pass to this destination and any synthetic back stack created
* due to this destination being added.
* @return this object for chaining
*/
@JvmOverloads
public fun setDestination(@IdRes destId: Int, args: Bundle? = null): NavDeepLinkBuilder {
destinations.clear()
destinations.add(DeepLinkDestination(destId, args))
if (graph != null) {
verifyAllDestinations()
}
return this
}
/**
* Sets the destination route to deep link to. Any destinations previous added via
* [.addDestination] are cleared, effectively resetting this object
* back to only this single destination.
*
* @param destRoute destination route to deep link to.
* @param args Arguments to pass to this destination and any synthetic back stack created
* due to this destination being added.
* @return this object for chaining
*/
@JvmOverloads
public fun setDestination(destRoute: String, args: Bundle? = null): NavDeepLinkBuilder {
destinations.clear()
destinations.add(DeepLinkDestination(createRoute(destRoute).hashCode(), args))
if (graph != null) {
verifyAllDestinations()
}
return this
}
/**
* Add a new destination id to deep link to. This builds off any previous calls to this method
* or calls to [setDestination], building the minimal synthetic back stack of
* start destinations between the previous deep link destination and the newly added
* deep link destination.
*
* This means that if R.navigation.nav_graph has startDestination= R.id.start_destination,
*
* ```
* navDeepLinkBuilder
* .setGraph(R.navigation.nav_graph)
* .addDestination(R.id.second_destination, null)
* ```
* is equivalent to
* ```
* navDeepLinkBuilder
* .setGraph(R.navigation.nav_graph)
* .addDestination(R.id.start_destination, null)
* .addDestination(R.id.second_destination, null)
* ```
*
* Use the second form to assign specific arguments to the start destination.
*
* @param destId destination ID to deep link to.
* @param args Arguments to pass to this destination and any synthetic back stack created
* due to this destination being added.
* @return this object for chaining
*/
@JvmOverloads
public fun addDestination(@IdRes destId: Int, args: Bundle? = null): NavDeepLinkBuilder {
destinations.add(DeepLinkDestination(destId, args))
if (graph != null) {
verifyAllDestinations()
}
return this
}
/**
* Add a new destination route to deep link to. This builds off any previous calls to this
* method or calls to [.setDestination], building the minimal synthetic back stack of
* start destinations between the previous deep link destination and the newly added
* deep link destination.
*
* @param route destination route to deep link to.
* @param args Arguments to pass to this destination and any synthetic back stack created
* due to this destination being added.
* @return this object for chaining
*/
@JvmOverloads
public fun addDestination(route: String, args: Bundle? = null): NavDeepLinkBuilder {
destinations.add(DeepLinkDestination(createRoute(route).hashCode(), args))
if (graph != null) {
verifyAllDestinations()
}
return this
}
private fun findDestination(@IdRes destId: Int): NavDestination? {
val possibleDestinations = ArrayDeque<NavDestination>()
possibleDestinations.add(graph!!)
while (!possibleDestinations.isEmpty()) {
val destination = possibleDestinations.removeFirst()
if (destination.id == destId) {
return destination
} else if (destination is NavGraph) {
for (child in destination) {
possibleDestinations.add(child)
}
}
}
return null
}
private fun verifyAllDestinations() {
for (destination in destinations) {
val destId = destination.destinationId
val node = findDestination(destId)
if (node == null) {
val dest = NavDestination.getDisplayName(context, destId)
throw IllegalArgumentException(
"Navigation destination $dest cannot be found in the navigation graph $graph"
)
}
}
}
private fun fillInIntent() {
val deepLinkIds = mutableListOf<Int>()
val deepLinkArgs = ArrayList<Bundle?>()
var previousDestination: NavDestination? = null
for (destination in destinations) {
val destId = destination.destinationId
val arguments = destination.arguments
val node = findDestination(destId)
if (node == null) {
val dest = NavDestination.getDisplayName(context, destId)
throw IllegalArgumentException(
"Navigation destination $dest cannot be found in the navigation graph $graph"
)
}
for (id in node.buildDeepLinkIds(previousDestination)) {
deepLinkIds.add(id)
deepLinkArgs.add(arguments)
}
previousDestination = node
}
val idArray = deepLinkIds.toIntArray()
intent.putExtra(NavController.KEY_DEEP_LINK_IDS, idArray)
intent.putParcelableArrayListExtra(NavController.KEY_DEEP_LINK_ARGS, deepLinkArgs)
}
/**
* Set optional arguments to send onto every destination created by this deep link.
* @param args arguments to pass to each destination
* @return this object for chaining
*/
public fun setArguments(args: Bundle?): NavDeepLinkBuilder {
globalArgs = args
intent.putExtra(NavController.KEY_DEEP_LINK_EXTRAS, args)
return this
}
/**
* Construct the full [task stack][TaskStackBuilder] needed to deep link to the given
* destination.
*
* You must have [set a NavGraph][setGraph] and [set a destination][setDestination]
* before calling this method.
*
* @return a [TaskStackBuilder] which can be used to
* [send the deep link][TaskStackBuilder.startActivities] or
* [create a PendingIntent][TaskStackBuilder.getPendingIntent] to deep link to
* the given destination.
*/
public fun createTaskStackBuilder(): TaskStackBuilder {
checkNotNull(graph) {
"You must call setGraph() before constructing the deep link"
}
check(destinations.isNotEmpty()) {
"You must call setDestination() or addDestination() before constructing the deep link"
}
fillInIntent()
// We create a copy of the Intent to ensure the Intent does not have itself
// as an extra. This also prevents developers from modifying the internal Intent
// via taskStackBuilder.editIntentAt()
val taskStackBuilder = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(Intent(intent))
for (index in 0 until taskStackBuilder.intentCount) {
// Attach the original Intent to each Activity so that they can know
// they were constructed in response to a deep link
taskStackBuilder.editIntentAt(index)
?.putExtra(NavController.KEY_DEEP_LINK_INTENT, intent)
}
return taskStackBuilder
}
/**
* Construct a [PendingIntent] to the [deep link destination][setDestination].
*
* This constructs the entire [task stack][createTaskStackBuilder] needed.
*
* You must have [set a NavGraph][setGraph] and [set a destination][setDestination]
* before calling this method.
*
* @return a PendingIntent constructed with [TaskStackBuilder.getPendingIntent] to deep link
* to the given destination
*/
public fun createPendingIntent(): PendingIntent {
var requestCode = 0
globalArgs?.let { globalArgs ->
for (key in globalArgs.keySet()) {
val value = globalArgs[key]
requestCode = 31 * requestCode + (value?.hashCode() ?: 0)
}
}
for (destination in destinations) {
val destId = destination.destinationId
requestCode = 31 * requestCode + destId
val arguments = destination.arguments
if (arguments != null) {
for (key in arguments.keySet()) {
val value = arguments[key]
requestCode = 31 * requestCode + (value?.hashCode() ?: 0)
}
}
}
return createTaskStackBuilder().getPendingIntent(
requestCode,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)!!
}
/**
* A [NavigatorProvider] that only parses the basics: [navigation graphs][NavGraph]
* and [destinations][NavDestination], effectively only getting the base destination
* information.
*/
private class PermissiveNavigatorProvider : NavigatorProvider() {
/**
* A Navigator that only parses the [NavDestination] attributes.
*/
private val mDestNavigator: Navigator<NavDestination> =
object : Navigator<NavDestination>() {
override fun createDestination(): NavDestination {
return NavDestination("permissive")
}
override fun navigate(
destination: NavDestination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Extras?
): NavDestination? {
throw IllegalStateException("navigate is not supported")
}
override fun popBackStack(): Boolean {
throw IllegalStateException("popBackStack is not supported")
}
}
@Suppress("UNCHECKED_CAST")
override fun <T : Navigator<out NavDestination>> getNavigator(name: String): T {
return try {
super.getNavigator(name)
} catch (e: IllegalStateException) {
mDestNavigator as T
}
}
init {
addNavigator(NavGraphNavigator(this))
}
}
}