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.app.Activity 19 import android.content.ComponentName 20 import android.content.Context 21 import android.content.ContextWrapper 22 import android.content.Intent 23 import android.net.Uri 24 import android.util.AttributeSet 25 import android.util.Log 26 import androidx.annotation.CallSuper 27 import androidx.annotation.RestrictTo 28 import androidx.core.app.ActivityCompat 29 import androidx.core.app.ActivityOptionsCompat 30 import androidx.core.content.res.use 31 import androidx.savedstate.SavedState 32 import androidx.savedstate.read 33 import java.util.regex.Pattern 34 35 /** ActivityNavigator implements cross-activity navigation. */ 36 @Navigator.Name("activity") 37 public open class ActivityNavigator( 38 @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public val context: Context 39 ) : Navigator<ActivityNavigator.Destination>() { 40 private val hostActivity: Activity? = 41 generateSequence(context) { 42 if (it is ContextWrapper) { 43 it.baseContext 44 } else null 45 } 46 .firstOrNull { it is Activity } as Activity? 47 48 override fun createDestination(): Destination { 49 return Destination(this) 50 } 51 52 override fun popBackStack(): Boolean { 53 if (hostActivity != null) { 54 hostActivity.finish() 55 return true 56 } 57 return false 58 } 59 60 /** 61 * Navigate to a destination. 62 * 63 * <p>Requests navigation to a given destination associated with this navigator in the 64 * navigation graph. This method generally should not be called directly; NavController will 65 * delegate to it when appropriate.</p> 66 * 67 * @param destination destination node to navigate to 68 * @param args arguments to use for navigation 69 * @param navOptions additional options for navigation 70 * @param navigatorExtras extras unique to your Navigator. 71 * @return The NavDestination that should be added to the back stack or null if no change was 72 * made to the back stack (i.e., in cases of single top operations where the destination is 73 * already on top of the back stack). 74 * @throws IllegalArgumentException if the given destination has no Intent 75 */ 76 @Suppress("DEPRECATION") 77 override fun navigate( 78 destination: Destination, 79 args: SavedState?, 80 navOptions: NavOptions?, 81 navigatorExtras: Navigator.Extras? 82 ): NavDestination? { 83 checkNotNull(destination.intent) { 84 ("Destination ${destination.id} does not have an Intent set.") 85 } 86 val intent = Intent(destination.intent) 87 if (args != null) { 88 intent.putExtras(args) 89 val dataPattern = destination.dataPattern 90 if (!dataPattern.isNullOrEmpty()) { 91 // Fill in the data pattern with the args to build a valid URI 92 val data = StringBuffer() 93 val fillInPattern = Pattern.compile("\\{(.+?)\\}") 94 val matcher = fillInPattern.matcher(dataPattern) 95 while (matcher.find()) { 96 args.read { 97 val argName = matcher.group(1)!! 98 require(contains(argName)) { 99 "Could not find $argName in $args to fill data pattern $dataPattern" 100 } 101 matcher.appendReplacement(data, "") 102 // Serialize with NavType if present, otherwise fallback to toString() 103 val navType = destination.arguments[argName]?.type 104 val value = 105 navType?.serializeAsValue(navType[args, argName]) 106 ?: Uri.encode(args.get(argName).toString()) 107 data.append(value) 108 } 109 } 110 matcher.appendTail(data) 111 intent.data = Uri.parse(data.toString()) 112 } 113 } 114 if (navigatorExtras is Extras) { 115 intent.addFlags(navigatorExtras.flags) 116 } 117 if (hostActivity == null) { 118 // If we're not launching from an Activity context we have to launch in a new task. 119 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 120 } 121 if (navOptions != null && navOptions.shouldLaunchSingleTop()) { 122 intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) 123 } 124 if (hostActivity != null) { 125 val hostIntent = hostActivity.intent 126 if (hostIntent != null) { 127 val hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0) 128 if (hostCurrentId != 0) { 129 intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId) 130 } 131 } 132 } 133 val destId = destination.id 134 intent.putExtra(EXTRA_NAV_CURRENT, destId) 135 val resources = context.resources 136 if (navOptions != null) { 137 val popEnterAnim = navOptions.popEnterAnim 138 val popExitAnim = navOptions.popExitAnim 139 if ( 140 popEnterAnim > 0 && resources.getResourceTypeName(popEnterAnim) == "animator" || 141 popExitAnim > 0 && resources.getResourceTypeName(popExitAnim) == "animator" 142 ) { 143 Log.w( 144 LOG_TAG, 145 "Activity destinations do not support Animator resource. Ignoring " + 146 "popEnter resource ${resources.getResourceName(popEnterAnim)} and " + 147 "popExit resource ${resources.getResourceName(popExitAnim)} when " + 148 "launching $destination" 149 ) 150 } else { 151 // For use in applyPopAnimationsToPendingTransition() 152 intent.putExtra(EXTRA_POP_ENTER_ANIM, popEnterAnim) 153 intent.putExtra(EXTRA_POP_EXIT_ANIM, popExitAnim) 154 } 155 } 156 if (navigatorExtras is Extras) { 157 val activityOptions = navigatorExtras.activityOptions 158 if (activityOptions != null) { 159 ActivityCompat.startActivity(context, intent, activityOptions.toBundle()) 160 } else { 161 context.startActivity(intent) 162 } 163 } else { 164 context.startActivity(intent) 165 } 166 if (navOptions != null && hostActivity != null) { 167 var enterAnim = navOptions.enterAnim 168 var exitAnim = navOptions.exitAnim 169 if ( 170 enterAnim > 0 && (resources.getResourceTypeName(enterAnim) == "animator") || 171 exitAnim > 0 && (resources.getResourceTypeName(exitAnim) == "animator") 172 ) { 173 Log.w( 174 LOG_TAG, 175 "Activity destinations do not support Animator resource. " + 176 "Ignoring " + 177 "enter resource " + 178 resources.getResourceName(enterAnim) + 179 " and exit resource " + 180 resources.getResourceName(exitAnim) + 181 "when " + 182 "launching " + 183 destination 184 ) 185 } else if (enterAnim >= 0 || exitAnim >= 0) { 186 enterAnim = enterAnim.coerceAtLeast(0) 187 exitAnim = exitAnim.coerceAtLeast(0) 188 hostActivity.overridePendingTransition(enterAnim, exitAnim) 189 } 190 } 191 192 // You can't pop the back stack from the caller of a new Activity, 193 // so we don't add this navigator to the controller's back stack 194 return null 195 } 196 197 /** 198 * NavDestination for activity navigation 199 * 200 * Construct a new activity destination. This destination is not valid until you set the Intent 201 * via [setIntent] or one or more of the other set method. 202 * 203 * @param activityNavigator The [ActivityNavigator] which this destination will be associated 204 * with. Generally retrieved via a [NavController]'s [NavigatorProvider.getNavigator] method. 205 */ 206 @NavDestination.ClassType(Activity::class) 207 public open class Destination(activityNavigator: Navigator<out Destination>) : 208 NavDestination(activityNavigator) { 209 /** The Intent associated with this destination. */ 210 public var intent: Intent? = null 211 private set 212 213 /** The dynamic data URI pattern, if any */ 214 public var dataPattern: String? = null 215 private set 216 217 /** 218 * Set the Intent to start when navigating to this destination. 219 * 220 * @param intent Intent to associated with this destination. 221 * @return this [Destination] 222 */ 223 public fun setIntent(intent: Intent?): Destination { 224 this.intent = intent 225 return this 226 } 227 228 /** 229 * Sets a dynamic data URI pattern that is sent when navigating to this destination. 230 * 231 * If a non-null arguments Bundle is present when navigating, any segments in the form 232 * `{argName}` will be replaced with a URI encoded string from the arguments. 233 * 234 * When inflated from XML, you can use `${applicationId}` as an argument pattern to 235 * automatically use [Context.getPackageName]. 236 * 237 * @param dataPattern A URI pattern with segments in the form of `{argName}` that will be 238 * replaced with URI encoded versions of the Strings in the arguments Bundle. 239 * @return this [Destination] 240 * @see Destination.setData 241 */ 242 public fun setDataPattern(dataPattern: String?): Destination { 243 this.dataPattern = dataPattern 244 return this 245 } 246 247 /** 248 * Construct a new activity destination. This destination is not valid until you set the 249 * Intent via [setIntent] or one or more of the other set method. 250 * 251 * @param navigatorProvider The [NavController] which this destination will be associated 252 * with. 253 */ 254 public constructor( 255 navigatorProvider: NavigatorProvider 256 ) : this(navigatorProvider.getNavigator(ActivityNavigator::class.java)) 257 258 @CallSuper 259 override fun onInflate(context: Context, attrs: AttributeSet) { 260 super.onInflate(context, attrs) 261 context.resources.obtainAttributes(attrs, R.styleable.ActivityNavigator).use { array -> 262 var targetPackage = 263 parseApplicationId( 264 context, 265 array.getString(R.styleable.ActivityNavigator_targetPackage) 266 ) 267 setTargetPackage(targetPackage) 268 var className = array.getString(R.styleable.ActivityNavigator_android_name) 269 if (className != null) { 270 if (className[0] == '.') { 271 className = context.packageName + className 272 } 273 setComponentName(ComponentName(context, className)) 274 } 275 setAction(array.getString(R.styleable.ActivityNavigator_action)) 276 val data = 277 parseApplicationId(context, array.getString(R.styleable.ActivityNavigator_data)) 278 if (data != null) { 279 setData(Uri.parse(data)) 280 } 281 val dataPattern = 282 parseApplicationId( 283 context, 284 array.getString(R.styleable.ActivityNavigator_dataPattern) 285 ) 286 setDataPattern(dataPattern) 287 } 288 } 289 290 private fun parseApplicationId(context: Context, pattern: String?): String? { 291 return pattern?.replace(NavInflater.APPLICATION_ID_PLACEHOLDER, context.packageName) 292 } 293 294 /** The explicit application package name associated with this destination, if any */ 295 public var targetPackage: String? = null 296 private set 297 get() = intent?.`package` 298 299 /** 300 * Set an explicit application package name that limits the components this destination will 301 * navigate to. 302 * 303 * When inflated from XML, you can use `${applicationId}` as the package name to 304 * automatically use [Context.getPackageName]. 305 * 306 * @param packageName packageName to set 307 * @return this [Destination] 308 */ 309 public fun setTargetPackage(packageName: String?): Destination { 310 if (intent == null) { 311 intent = Intent() 312 } 313 intent!!.setPackage(packageName) 314 return this 315 } 316 317 /** The explicit [ComponentName] associated with this destination, if any */ 318 public var component: ComponentName? = null 319 private set 320 get() = intent?.component 321 322 /** 323 * Set an explicit [ComponentName] to navigate to. 324 * 325 * @param name The component name of the Activity to start. 326 * @return this [Destination] 327 */ 328 public fun setComponentName(name: ComponentName?): Destination { 329 if (intent == null) { 330 intent = Intent() 331 } 332 intent!!.component = name 333 return this 334 } 335 336 /** The action used to start the Activity, if any */ 337 public var action: String? = null 338 private set 339 get() = intent?.action 340 341 /** 342 * Sets the action sent when navigating to this destination. 343 * 344 * @param action The action string to use. 345 * @return this [Destination] 346 */ 347 public fun setAction(action: String?): Destination { 348 if (intent == null) { 349 intent = Intent() 350 } 351 intent!!.action = action 352 return this 353 } 354 355 /** The data URI used to start the Activity, if any */ 356 public var data: Uri? = null 357 private set 358 get() = intent?.data 359 360 /** 361 * Sets a static data URI that is sent when navigating to this destination. 362 * 363 * To use a dynamic URI that changes based on the arguments passed in when navigating, use 364 * [setDataPattern], which will take precedence when arguments are present. 365 * 366 * When inflated from XML, you can use `${applicationId}` for string interpolation to 367 * automatically use [Context.getPackageName]. 368 * 369 * @param data A static URI that should always be used. 370 * @return this [Destination] 371 * @see Destination.setDataPattern 372 */ 373 public fun setData(data: Uri?): Destination { 374 if (intent == null) { 375 intent = Intent() 376 } 377 intent!!.data = data 378 return this 379 } 380 381 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 382 public override fun supportsActions(): Boolean { 383 return false 384 } 385 386 override fun toString(): String { 387 val componentName = component 388 val sb = StringBuilder() 389 sb.append(super.toString()) 390 if (componentName != null) { 391 sb.append(" class=") 392 sb.append(componentName.className) 393 } else { 394 val action = action 395 if (action != null) { 396 sb.append(" action=") 397 sb.append(action) 398 } 399 } 400 return sb.toString() 401 } 402 403 override fun equals(other: Any?): Boolean { 404 if (this === other) return true 405 if (other == null || other !is Destination) return false 406 return super.equals(other) && 407 intent?.filterEquals(other.intent) ?: (other.intent == null) && 408 dataPattern == other.dataPattern 409 } 410 411 override fun hashCode(): Int { 412 var result = super.hashCode() 413 result = 31 * result + (intent?.filterHashCode() ?: 0) 414 result = 31 * result + dataPattern.hashCode() 415 return result 416 } 417 } 418 419 /** 420 * Extras that can be passed to ActivityNavigator to customize what [ActivityOptionsCompat] and 421 * flags are passed through to the call to [ActivityCompat.startActivity]. 422 */ 423 public class Extras 424 internal constructor( 425 /** The `Intent.FLAG_ACTIVITY_` flags that should be added to the Intent. */ 426 public val flags: Int, 427 /** The [ActivityOptionsCompat] that should be used with [ActivityCompat.startActivity]. */ 428 public val activityOptions: ActivityOptionsCompat? 429 ) : Navigator.Extras { 430 431 /** 432 * Builder for constructing new [Extras] instances. The resulting instances are immutable. 433 */ 434 public class Builder { 435 private var flags = 0 436 private var activityOptions: ActivityOptionsCompat? = null 437 438 /** 439 * Adds one or more `Intent.FLAG_ACTIVITY_` flags 440 * 441 * @param flags the flags to add 442 * @return this [Builder] 443 */ 444 public fun addFlags(flags: Int): Builder { 445 this.flags = this.flags or flags 446 return this 447 } 448 449 /** 450 * Sets the [ActivityOptionsCompat] that should be used with 451 * [ActivityCompat.startActivity]. 452 * 453 * @param activityOptions The [ActivityOptionsCompat] to pass through 454 * @return this [Builder] 455 */ 456 public fun setActivityOptions(activityOptions: ActivityOptionsCompat): Builder { 457 this.activityOptions = activityOptions 458 return this 459 } 460 461 /** 462 * Constructs the final [Extras] instance. 463 * 464 * @return An immutable [Extras] instance. 465 */ 466 public fun build(): Extras { 467 return Extras(flags, activityOptions) 468 } 469 } 470 } 471 472 public companion object { 473 private const val EXTRA_NAV_SOURCE = "android-support-navigation:ActivityNavigator:source" 474 private const val EXTRA_NAV_CURRENT = "android-support-navigation:ActivityNavigator:current" 475 private const val EXTRA_POP_ENTER_ANIM = 476 "android-support-navigation:ActivityNavigator:popEnterAnim" 477 private const val EXTRA_POP_EXIT_ANIM = 478 "android-support-navigation:ActivityNavigator:popExitAnim" 479 private const val LOG_TAG = "ActivityNavigator" 480 481 /** 482 * Apply any pop animations in the Intent of the given Activity to a pending transition. 483 * This should be used in place of [Activity.overridePendingTransition] to get the 484 * appropriate pop animations. 485 * 486 * @param activity An activity started from the [ActivityNavigator]. 487 * @see NavOptions.popEnterAnim 488 * @see NavOptions.popExitAnim 489 */ 490 @Suppress("DEPRECATION") 491 @JvmStatic 492 public fun applyPopAnimationsToPendingTransition(activity: Activity) { 493 val intent = activity.intent ?: return 494 var popEnterAnim = intent.getIntExtra(EXTRA_POP_ENTER_ANIM, -1) 495 var popExitAnim = intent.getIntExtra(EXTRA_POP_EXIT_ANIM, -1) 496 if (popEnterAnim != -1 || popExitAnim != -1) { 497 popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0 498 popExitAnim = if (popExitAnim != -1) popExitAnim else 0 499 activity.overridePendingTransition(popEnterAnim, popExitAnim) 500 } 501 } 502 } 503 } 504