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