1 /*
<lambda>null2  * 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.content.Context
19 import android.net.Uri
20 import android.util.AttributeSet
21 import androidx.annotation.CallSuper
22 import androidx.annotation.IdRes
23 import androidx.annotation.RestrictTo
24 import androidx.collection.SparseArrayCompat
25 import androidx.collection.keyIterator
26 import androidx.collection.valueIterator
27 import androidx.core.content.res.use
28 import androidx.navigation.common.R
29 import androidx.navigation.internal.NavContext
30 import androidx.navigation.internal.NavDestinationImpl
31 import androidx.navigation.serialization.generateHashCode
32 import androidx.savedstate.SavedState
33 import androidx.savedstate.read
34 import java.util.regex.Pattern
35 import kotlin.reflect.KClass
36 import kotlinx.serialization.InternalSerializationApi
37 import kotlinx.serialization.serializer
38 
39 public actual open class NavDestination
40 actual constructor(public actual val navigatorName: String) {
41 
42     private val impl = NavDestinationImpl(this)
43 
44     @Retention(AnnotationRetention.BINARY)
45     @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
46     public actual annotation class ClassType(actual val value: KClass<*>)
47 
48     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
49     public actual class DeepLinkMatch
50     actual constructor(
51         public actual val destination: NavDestination,
52         @get:Suppress("NullableCollection") // Needed for nullable savedState
53         public actual val matchingArgs: SavedState?,
54         private val isExactDeepLink: Boolean,
55         private val matchingPathSegments: Int,
56         private val hasMatchingAction: Boolean,
57         private val mimeTypeMatchLevel: Int
58     ) : Comparable<DeepLinkMatch> {
59         override fun compareTo(other: DeepLinkMatch): Int {
60             // Prefer exact deep links
61             if (isExactDeepLink && !other.isExactDeepLink) {
62                 return 1
63             } else if (!isExactDeepLink && other.isExactDeepLink) {
64                 return -1
65             }
66             // Then prefer most exact match path segments
67             val pathSegmentDifference = matchingPathSegments - other.matchingPathSegments
68             if (pathSegmentDifference > 0) {
69                 return 1
70             } else if (pathSegmentDifference < 0) {
71                 return -1
72             }
73             if (matchingArgs != null && other.matchingArgs == null) {
74                 return 1
75             } else if (matchingArgs == null && other.matchingArgs != null) {
76                 return -1
77             }
78             if (matchingArgs != null) {
79                 val sizeDifference =
80                     matchingArgs.read { size() } - other.matchingArgs!!.read { size() }
81                 if (sizeDifference > 0) {
82                     return 1
83                 } else if (sizeDifference < 0) {
84                     return -1
85                 }
86             }
87             if (hasMatchingAction && !other.hasMatchingAction) {
88                 return 1
89             } else if (!hasMatchingAction && other.hasMatchingAction) {
90                 return -1
91             }
92             return mimeTypeMatchLevel - other.mimeTypeMatchLevel
93         }
94 
95         public actual fun hasMatchingArgs(arguments: SavedState?): Boolean {
96             if (arguments == null || matchingArgs == null) return false
97 
98             matchingArgs.keySet().forEach { key ->
99                 // the arguments must at least contain every argument stored in this deep link
100                 if (!arguments.read { contains(key) }) return false
101 
102                 val type = destination.arguments[key]?.type
103                 val matchingArgValue = type?.get(matchingArgs, key)
104                 val entryArgValue = type?.get(arguments, key)
105                 if (type?.valueEquals(matchingArgValue, entryArgValue) == false) {
106                     return false
107                 }
108             }
109             return true
110         }
111     }
112 
113     public actual var parent: NavGraph? = null
114         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public set
115 
116     private var idName: String? by impl::idName
117 
118     public actual var label: CharSequence? = null
119     private val deepLinks by impl::deepLinks
120 
121     private val actions: SparseArrayCompat<NavAction> = SparseArrayCompat()
122 
123     public actual val arguments: Map<String, NavArgument>
124         get() = impl.arguments.toMap()
125 
126     public actual constructor(
127         navigator: Navigator<out NavDestination>
128     ) : this(NavigatorProvider.getNameForNavigator(navigator.javaClass))
129 
130     /**
131      * Called when inflating a destination from a resource.
132      *
133      * @param context local context performing inflation
134      * @param attrs attrs to parse during inflation
135      */
136     @CallSuper
137     public open fun onInflate(context: Context, attrs: AttributeSet) {
138         context.resources.obtainAttributes(attrs, R.styleable.Navigator).use { array ->
139             route = array.getString(R.styleable.Navigator_route)
140 
141             if (array.hasValue(R.styleable.Navigator_android_id)) {
142                 id = array.getResourceId(R.styleable.Navigator_android_id, 0)
143                 idName = getDisplayName(NavContext(context), id)
144             }
145             label = array.getText(R.styleable.Navigator_android_label)
146         }
147     }
148 
149     /**
150      * The destination's unique ID. This should be an ID resource generated by the Android resource
151      * system.
152      *
153      * If using safe args, setting this manually will override the ID that was set based on route
154      * from KClass.
155      */
156     @get:IdRes
157     public actual var id: Int
158         get() = impl.id
159         set(@IdRes value) {
160             impl.id = value
161         }
162 
163     public actual var route: String? by impl::route
164 
165     public actual open val displayName: String
166         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get() = idName ?: id.toString()
167 
168     public actual open fun hasDeepLink(deepLink: Uri): Boolean {
169         return hasDeepLink(NavDeepLinkRequest(deepLink, null, null))
170     }
171 
172     public actual open fun hasDeepLink(deepLinkRequest: NavDeepLinkRequest): Boolean {
173         return matchDeepLink(deepLinkRequest) != null
174     }
175 
176     public actual fun addDeepLink(uriPattern: String) {
177         addDeepLink(NavDeepLink.Builder().setUriPattern(uriPattern).build())
178     }
179 
180     public actual fun addDeepLink(navDeepLink: NavDeepLink) {
181         impl.addDeepLink(navDeepLink)
182     }
183 
184     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
185     public actual fun matchRoute(route: String): DeepLinkMatch? {
186         return impl.matchRoute(route)
187     }
188 
189     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
190     public actual open fun matchDeepLink(navDeepLinkRequest: NavDeepLinkRequest): DeepLinkMatch? {
191         return impl.matchDeepLink(navDeepLinkRequest)
192     }
193 
194     /**
195      * Build an array containing the hierarchy from the root down to this destination.
196      *
197      * @param previousDestination the previous destination we are starting at
198      * @return An array containing all of the ids from the previous destination (or the root of the
199      *   graph if null) to this destination
200      */
201     @JvmOverloads
202     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
203     public fun buildDeepLinkIds(previousDestination: NavDestination? = null): IntArray {
204         val hierarchy = ArrayDeque<NavDestination>()
205         var current: NavDestination? = this
206         do {
207             val parent = current!!.parent
208             if (
209                 // If the current destination is a sibling of the previous, just add it straightaway
210                 previousDestination?.parent != null &&
211                     previousDestination.parent!!.findNode(current.id) === current
212             ) {
213                 hierarchy.addFirst(current)
214                 break
215             }
216             if (parent == null || parent.startDestinationId != current.id) {
217                 hierarchy.addFirst(current)
218             }
219             if (parent == previousDestination) {
220                 break
221             }
222             current = parent
223         } while (current != null)
224         return hierarchy.toList().map { it.id }.toIntArray()
225     }
226 
227     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
228     public actual fun hasRoute(route: String, arguments: SavedState?): Boolean {
229         return impl.hasRoute(route, arguments)
230     }
231 
232     /**
233      * @return Whether this NavDestination supports outgoing actions
234      * @see NavDestination.putAction
235      */
236     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
237     public open fun supportsActions(): Boolean {
238         return true
239     }
240 
241     /**
242      * Returns the [NavAction] for the given action ID. This will recursively check the
243      * [parent][getParent] of this destination if the action destination is not found in this
244      * destination.
245      *
246      * @param id action ID to fetch
247      * @return the [NavAction] mapped to the given action id, or null if one has not been set
248      */
249     public fun getAction(@IdRes id: Int): NavAction? {
250         val destination = if (actions.isEmpty) null else actions[id]
251         // Search the parent for the given action if it is not found in this destination
252         return destination ?: parent?.run { getAction(id) }
253     }
254 
255     /**
256      * Creates a [NavAction] for the given [destId] and associates it with the [actionId].
257      *
258      * @param actionId action ID to bind
259      * @param destId destination ID for the given action
260      */
261     public fun putAction(@IdRes actionId: Int, @IdRes destId: Int) {
262         putAction(actionId, NavAction(destId))
263     }
264 
265     /**
266      * Sets the [NavAction] destination for an action ID.
267      *
268      * @param actionId action ID to bind
269      * @param action action to associate with this action ID
270      * @throws UnsupportedOperationException this destination is considered a terminal destination
271      *   and does not support actions
272      */
273     public fun putAction(@IdRes actionId: Int, action: NavAction) {
274         if (!supportsActions()) {
275             throw UnsupportedOperationException(
276                 "Cannot add action $actionId to $this as it does not support actions, " +
277                     "indicating that it is a terminal destination in your navigation graph and " +
278                     "will never trigger actions."
279             )
280         }
281         require(actionId != 0) { "Cannot have an action with actionId 0" }
282         actions.put(actionId, action)
283     }
284 
285     /**
286      * Unsets the [NavAction] for an action ID.
287      *
288      * @param actionId action ID to remove
289      */
290     public fun removeAction(@IdRes actionId: Int) {
291         actions.remove(actionId)
292     }
293 
294     public actual fun addArgument(argumentName: String, argument: NavArgument) {
295         impl.addArgument(argumentName, argument)
296     }
297 
298     public actual fun removeArgument(argumentName: String) {
299         impl.removeArgument(argumentName)
300     }
301 
302     @Suppress("NullableCollection") // Needed for nullable savedState
303     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
304     public actual fun addInDefaultArgs(args: SavedState?): SavedState? {
305         return impl.addInDefaultArgs(args)
306     }
307 
308     /**
309      * Parses a dynamic label containing arguments into a String.
310      *
311      * Supports String Resource arguments by parsing `R.string` values of `ReferenceType` arguments
312      * found in `android:label` into their String values.
313      *
314      * Returns `null` if label is null.
315      *
316      * Returns the original label if the label was a static string.
317      *
318      * @param context Context used to resolve a resource's name
319      * @param bundle SavedState containing the arguments used in the label
320      * @return The parsed string or null if the label is null
321      * @throws IllegalArgumentException if an argument provided in the label cannot be found in the
322      *   bundle, or if the label contains a string template but the bundle is null
323      */
324     public fun fillInLabel(context: Context, bundle: SavedState?): String? {
325         val label = label ?: return null
326 
327         val fillInPattern = Pattern.compile("\\{(.+?)\\}")
328         val matcher = fillInPattern.matcher(label)
329         val builder = StringBuffer()
330 
331         val args = bundle?.read { toMap() } ?: emptyMap()
332         while (matcher.find()) {
333             val argName = matcher.group(1)
334             require(argName != null && argName in args) {
335                 "Could not find \"$argName\" in $bundle to fill label \"$label\""
336             }
337 
338             matcher.appendReplacement(builder, /* replacement= */ "")
339 
340             val argType = arguments[argName]?.type
341             val argValue =
342                 if (argType == NavType.ReferenceType) {
343                     context.getString(/* resId= */ NavType.ReferenceType[bundle!!, argName] as Int)
344                 } else {
345                     argType!![bundle!!, argName].toString()
346                 }
347             builder.append(argValue)
348         }
349         matcher.appendTail(builder)
350         return builder.toString()
351     }
352 
353     override fun toString(): String {
354         val sb = StringBuilder()
355         sb.append(javaClass.simpleName)
356         sb.append("(")
357         if (idName == null) {
358             sb.append("0x")
359             sb.append(Integer.toHexString(id))
360         } else {
361             sb.append(idName)
362         }
363         sb.append(")")
364         if (!route.isNullOrBlank()) {
365             sb.append(" route=")
366             sb.append(route)
367         }
368         if (label != null) {
369             sb.append(" label=")
370             sb.append(label)
371         }
372         return sb.toString()
373     }
374 
375     override fun equals(other: Any?): Boolean {
376         if (this === other) return true
377         if (other == null || other !is NavDestination) return false
378 
379         val equalDeepLinks = deepLinks == other.deepLinks
380 
381         val equalActions =
382             actions.size() == other.actions.size() &&
383                 actions.keyIterator().asSequence().all { actions.get(it) == other.actions.get(it) }
384 
385         val equalArguments =
386             arguments.size == other.arguments.size &&
387                 arguments.asSequence().all {
388                     other.arguments.containsKey(it.key) && other.arguments[it.key] == it.value
389                 }
390 
391         return id == other.id &&
392             route == other.route &&
393             equalDeepLinks &&
394             equalActions &&
395             equalArguments
396     }
397 
398     @Suppress("DEPRECATION")
399     override fun hashCode(): Int {
400         var result = id
401         result = 31 * result + route.hashCode()
402         deepLinks.forEach {
403             result = 31 * result + it.uriPattern.hashCode()
404             result = 31 * result + it.action.hashCode()
405             result = 31 * result + it.mimeType.hashCode()
406         }
407         actions.valueIterator().forEach { value ->
408             result = 31 * result + value.destinationId
409             result = 31 * result + value.navOptions.hashCode()
410             value.defaultArguments?.read { result = 31 * result + contentDeepHashCode() }
411         }
412         arguments.keys.forEach {
413             result = 31 * result + it.hashCode()
414             result = 31 * result + arguments[it].hashCode()
415         }
416         return result
417     }
418 
419     public actual companion object {
420         private val classes = mutableMapOf<String, Class<*>>()
421 
422         /**
423          * Parse the class associated with this destination from a raw name, generally extracted
424          * from the `android:name` attribute added to the destination's XML. This should be the
425          * class providing the visual representation of the destination that the user sees after
426          * navigating to this destination.
427          *
428          * This method does name -> Class caching and should be strongly preferred over doing your
429          * own parsing if your [Navigator] supports the `android:name` attribute to give consistent
430          * behavior across all Navigators.
431          *
432          * @param context Context providing the package name for use with relative class names and
433          *   the ClassLoader
434          * @param name Absolute or relative class name. Null names will be ignored.
435          * @param expectedClassType The expected class type
436          * @return The parsed class
437          * @throws IllegalArgumentException if the class is not found in the provided Context's
438          *   ClassLoader or if the class is not of the expected type
439          */
440         @Suppress("UNCHECKED_CAST")
441         @JvmStatic
442         protected fun <C> parseClassFromName(
443             context: Context,
444             name: String,
445             expectedClassType: Class<out C?>
446         ): Class<out C?> {
447             var innerName = name
448             if (innerName[0] == '.') {
449                 innerName = context.packageName + innerName
450             }
451             var clazz = classes[innerName]
452             if (clazz == null) {
453                 try {
454                     clazz = Class.forName(innerName, true, context.classLoader)
455                     classes[name] = clazz
456                 } catch (e: ClassNotFoundException) {
457                     throw IllegalArgumentException(e)
458                 }
459             }
460             require(expectedClassType.isAssignableFrom(clazz!!)) {
461                 "$innerName must be a subclass of $expectedClassType"
462             }
463             return clazz as Class<out C?>
464         }
465 
466         /** Used internally for NavDestinationTest */
467         @JvmStatic
468         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
469         public fun <C> parseClassFromNameInternal(
470             context: Context,
471             name: String,
472             expectedClassType: Class<out C?>
473         ): Class<out C?> {
474             return parseClassFromName(context, name, expectedClassType)
475         }
476 
477         /**
478          * Retrieve a suitable display name for a given id.
479          *
480          * @param context Context used to resolve a resource's name
481          * @param id The id to get a display name for
482          * @return The resource's name if it is a valid id or just the id itself if it is not a
483          *   valid resource
484          */
485         @JvmStatic
486         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
487         public actual fun getDisplayName(context: NavContext, id: Int): String {
488             // aapt-generated IDs have the high byte nonzero,
489             // so anything below that cannot be a valid resource id
490             return if (id <= 0x00FFFFFF) {
491                 id.toString()
492             } else {
493                 context.getResourceName(id)
494             }
495         }
496 
497         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
498         public actual fun createRoute(route: String?): String =
499             if (route != null) "android-app://androidx.navigation/$route" else ""
500 
501         @JvmStatic
502         public actual val NavDestination.hierarchy: Sequence<NavDestination>
503             get() = generateSequence(this) { it.parent }
504 
505         @JvmStatic
506         public actual inline fun <reified T : Any> NavDestination.hasRoute(): Boolean =
507             hasRoute(T::class)
508 
509         @OptIn(InternalSerializationApi::class)
510         @JvmStatic
511         public actual fun <T : Any> NavDestination.hasRoute(route: KClass<T>): Boolean =
512             route.serializer().generateHashCode() == id
513     }
514 }
515