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