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