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.annotation.SuppressLint
19 import android.content.Context
20 import android.content.res.Resources
21 import android.content.res.TypedArray
22 import android.content.res.XmlResourceParser
23 import android.util.AttributeSet
24 import android.util.TypedValue
25 import android.util.Xml
26 import androidx.annotation.NavigationRes
27 import androidx.annotation.RestrictTo
28 import androidx.core.content.res.use
29 import androidx.core.content.withStyledAttributes
30 import androidx.navigation.common.R
31 import androidx.savedstate.SavedState
32 import androidx.savedstate.read
33 import androidx.savedstate.savedState
34 import java.io.IOException
35 import org.xmlpull.v1.XmlPullParser
36 import org.xmlpull.v1.XmlPullParserException
37 
38 /** Class which translates a navigation XML file into a [NavGraph] */
39 public class NavInflater(
40     private val context: Context,
41     private val navigatorProvider: NavigatorProvider
42 ) {
43     /**
44      * Inflate a NavGraph from the given XML resource id.
45      *
46      * @param graphResId
47      * @return
48      */
49     @SuppressLint("ResourceType")
50     public fun inflate(@NavigationRes graphResId: Int): NavGraph {
51         val res = context.resources
52         val parser = res.getXml(graphResId)
53         val attrs = Xml.asAttributeSet(parser)
54         return try {
55             var type: Int
56             while (
57                 parser.next().also { type = it } != XmlPullParser.START_TAG &&
58                     type != XmlPullParser.END_DOCUMENT
59             ) {
60                 /* Empty loop */
61             }
62             if (type != XmlPullParser.START_TAG) {
63                 throw XmlPullParserException("No start tag found")
64             }
65             val rootElement = parser.name
66             val destination = inflate(res, parser, attrs, graphResId)
67             require(destination is NavGraph) {
68                 "Root element <$rootElement> did not inflate into a NavGraph"
69             }
70             destination
71         } catch (e: Exception) {
72             throw RuntimeException(
73                 "Exception inflating ${res.getResourceName(graphResId)} line ${parser.lineNumber}",
74                 e
75             )
76         } finally {
77             parser.close()
78         }
79     }
80 
81     @Throws(XmlPullParserException::class, IOException::class)
82     private fun inflate(
83         res: Resources,
84         parser: XmlResourceParser,
85         attrs: AttributeSet,
86         graphResId: Int
87     ): NavDestination {
88         val navigator = navigatorProvider.getNavigator<Navigator<*>>(parser.name)
89         val dest = navigator.createDestination()
90         dest.onInflate(context, attrs)
91         val innerDepth = parser.depth + 1
92         var type: Int
93         var depth = 0
94         while (
95             parser.next().also { type = it } != XmlPullParser.END_DOCUMENT &&
96                 (parser.depth.also { depth = it } >= innerDepth || type != XmlPullParser.END_TAG)
97         ) {
98             if (type != XmlPullParser.START_TAG) {
99                 continue
100             }
101             if (depth > innerDepth) {
102                 continue
103             }
104             val name = parser.name
105             if (TAG_ARGUMENT == name) {
106                 inflateArgumentForDestination(res, dest, attrs, graphResId)
107             } else if (TAG_DEEP_LINK == name) {
108                 inflateDeepLink(res, dest, attrs)
109             } else if (TAG_ACTION == name) {
110                 inflateAction(res, dest, attrs, parser, graphResId)
111             } else if (TAG_INCLUDE == name && dest is NavGraph) {
112                 res.obtainAttributes(attrs, androidx.navigation.R.styleable.NavInclude).use {
113                     val id = it.getResourceId(androidx.navigation.R.styleable.NavInclude_graph, 0)
114                     dest.addDestination(inflate(id))
115                 }
116             } else if (dest is NavGraph) {
117                 dest.addDestination(inflate(res, parser, attrs, graphResId))
118             }
119         }
120         return dest
121     }
122 
123     @Throws(XmlPullParserException::class)
124     private fun inflateArgumentForDestination(
125         res: Resources,
126         dest: NavDestination,
127         attrs: AttributeSet,
128         graphResId: Int
129     ) {
130         res.obtainAttributes(attrs, R.styleable.NavArgument).use { array ->
131             val name =
132                 array.getString(R.styleable.NavArgument_android_name)
133                     ?: throw XmlPullParserException("Arguments must have a name")
134             val argument = inflateArgument(array, res, graphResId)
135             dest.addArgument(name, argument)
136         }
137     }
138 
139     @Throws(XmlPullParserException::class)
140     private fun inflateArgumentForBundle(
141         res: Resources,
142         savedState: SavedState,
143         attrs: AttributeSet,
144         graphResId: Int
145     ) {
146         res.obtainAttributes(attrs, R.styleable.NavArgument).use { array ->
147             val name =
148                 array.getString(R.styleable.NavArgument_android_name)
149                     ?: throw XmlPullParserException("Arguments must have a name")
150             val argument = inflateArgument(array, res, graphResId)
151             if (argument.isDefaultValuePresent) {
152                 argument.putDefaultValue(name, savedState)
153             }
154         }
155     }
156 
157     @Throws(XmlPullParserException::class)
158     private fun inflateArgument(a: TypedArray, res: Resources, graphResId: Int): NavArgument {
159         val argumentBuilder = NavArgument.Builder()
160         argumentBuilder.setIsNullable(a.getBoolean(R.styleable.NavArgument_nullable, false))
161         var value = sTmpValue.get()
162         if (value == null) {
163             value = TypedValue()
164             sTmpValue.set(value)
165         }
166         var defaultValue: Any? = null
167         var navType: NavType<*>? = null
168         val argType = a.getString(R.styleable.NavArgument_argType)
169         if (argType != null) {
170             navType = NavType.fromArgType(argType, res.getResourcePackageName(graphResId))
171         }
172         if (a.getValue(R.styleable.NavArgument_android_defaultValue, value)) {
173             if (navType === NavType.ReferenceType) {
174                 defaultValue =
175                     if (value.resourceId != 0) {
176                         value.resourceId
177                     } else if (value.type == TypedValue.TYPE_FIRST_INT && value.data == 0) {
178                         // Support "0" as a default value for reference types
179                         0
180                     } else {
181                         throw XmlPullParserException(
182                             "unsupported value '${value.string}' for ${navType.name}. Must be a " +
183                                 "reference to a resource."
184                         )
185                     }
186             } else if (value.resourceId != 0) {
187                 if (navType == null) {
188                     navType = NavType.ReferenceType
189                     defaultValue = value.resourceId
190                 } else {
191                     throw XmlPullParserException(
192                         "unsupported value '${value.string}' for ${navType.name}. You must use a " +
193                             "\"${NavType.ReferenceType.name}\" type to reference other resources."
194                     )
195                 }
196             } else if (navType === NavType.StringType) {
197                 defaultValue = a.getString(R.styleable.NavArgument_android_defaultValue)
198             } else {
199                 when (value.type) {
200                     TypedValue.TYPE_STRING -> {
201                         val stringValue = value.string.toString()
202                         if (navType == null) {
203                             navType = NavType.inferFromValue(stringValue)
204                         }
205                         defaultValue = navType.parseValue(stringValue)
206                     }
207                     TypedValue.TYPE_DIMENSION -> {
208                         navType =
209                             checkNavType(value, navType, NavType.IntType, argType, "dimension")
210                         defaultValue = value.getDimension(res.displayMetrics).toInt()
211                     }
212                     TypedValue.TYPE_FLOAT -> {
213                         navType = checkNavType(value, navType, NavType.FloatType, argType, "float")
214                         defaultValue = value.float
215                     }
216                     TypedValue.TYPE_INT_BOOLEAN -> {
217                         navType = checkNavType(value, navType, NavType.BoolType, argType, "boolean")
218                         defaultValue = value.data != 0
219                     }
220                     else ->
221                         if (
222                             value.type >= TypedValue.TYPE_FIRST_INT &&
223                                 value.type <= TypedValue.TYPE_LAST_INT
224                         ) {
225                             if (navType === NavType.FloatType) {
226                                 navType =
227                                     checkNavType(
228                                         value,
229                                         navType,
230                                         NavType.FloatType,
231                                         argType,
232                                         "float"
233                                     )
234                                 defaultValue = value.data.toFloat()
235                             } else {
236                                 navType =
237                                     checkNavType(
238                                         value,
239                                         navType,
240                                         NavType.IntType,
241                                         argType,
242                                         "integer"
243                                     )
244                                 defaultValue = value.data
245                             }
246                         } else {
247                             throw XmlPullParserException("unsupported argument type ${value.type}")
248                         }
249                 }
250             }
251         }
252         if (defaultValue != null) {
253             argumentBuilder.setDefaultValue(defaultValue)
254         }
255         if (navType != null) {
256             argumentBuilder.setType(navType)
257         }
258         return argumentBuilder.build()
259     }
260 
261     @Throws(XmlPullParserException::class)
262     private fun inflateDeepLink(res: Resources, dest: NavDestination, attrs: AttributeSet) {
263         res.obtainAttributes(attrs, R.styleable.NavDeepLink).use { array ->
264             val uri = array.getString(R.styleable.NavDeepLink_uri)
265             val action = array.getString(R.styleable.NavDeepLink_action)
266             val mimeType = array.getString(R.styleable.NavDeepLink_mimeType)
267             if (uri.isNullOrEmpty() && action.isNullOrEmpty() && mimeType.isNullOrEmpty()) {
268                 throw XmlPullParserException(
269                     "Every <$TAG_DEEP_LINK> must include at least one of app:uri, app:action, or " +
270                         "app:mimeType"
271                 )
272             }
273             val builder = NavDeepLink.Builder()
274             if (uri != null) {
275                 builder.setUriPattern(uri.replace(APPLICATION_ID_PLACEHOLDER, context.packageName))
276             }
277             if (!action.isNullOrEmpty()) {
278                 builder.setAction(action.replace(APPLICATION_ID_PLACEHOLDER, context.packageName))
279             }
280             if (mimeType != null) {
281                 builder.setMimeType(
282                     mimeType.replace(APPLICATION_ID_PLACEHOLDER, context.packageName)
283                 )
284             }
285             dest.addDeepLink(builder.build())
286         }
287     }
288 
289     @Throws(IOException::class, XmlPullParserException::class)
290     private fun inflateAction(
291         res: Resources,
292         dest: NavDestination,
293         attrs: AttributeSet,
294         parser: XmlResourceParser,
295         graphResId: Int
296     ) {
297         context.withStyledAttributes(attrs, R.styleable.NavAction) {
298             val id = getResourceId(R.styleable.NavAction_android_id, 0)
299             val destId = getResourceId(R.styleable.NavAction_destination, 0)
300             val action = NavAction(destId)
301             val builder = NavOptions.Builder()
302             builder.setLaunchSingleTop(getBoolean(R.styleable.NavAction_launchSingleTop, false))
303             builder.setRestoreState(getBoolean(R.styleable.NavAction_restoreState, false))
304             builder.setPopUpTo(
305                 getResourceId(R.styleable.NavAction_popUpTo, -1),
306                 getBoolean(R.styleable.NavAction_popUpToInclusive, false),
307                 getBoolean(R.styleable.NavAction_popUpToSaveState, false)
308             )
309             builder.setEnterAnim(getResourceId(R.styleable.NavAction_enterAnim, -1))
310             builder.setExitAnim(getResourceId(R.styleable.NavAction_exitAnim, -1))
311             builder.setPopEnterAnim(getResourceId(R.styleable.NavAction_popEnterAnim, -1))
312             builder.setPopExitAnim(getResourceId(R.styleable.NavAction_popExitAnim, -1))
313             action.navOptions = builder.build()
314             val args = savedState()
315             val innerDepth = parser.depth + 1
316             var type: Int
317             var depth = 0
318             while (
319                 parser.next().also { type = it } != XmlPullParser.END_DOCUMENT &&
320                     (parser.depth.also { depth = it } >= innerDepth ||
321                         type != XmlPullParser.END_TAG)
322             ) {
323                 if (type != XmlPullParser.START_TAG) {
324                     continue
325                 }
326                 if (depth > innerDepth) {
327                     continue
328                 }
329                 val name = parser.name
330                 if (TAG_ARGUMENT == name) {
331                     inflateArgumentForBundle(res, args, attrs, graphResId)
332                 }
333             }
334             if (!args.read { isEmpty() }) {
335                 action.defaultArguments = args
336             }
337             dest.putAction(id, action)
338         }
339     }
340 
341     public companion object {
342         private const val TAG_ARGUMENT = "argument"
343         private const val TAG_DEEP_LINK = "deepLink"
344         private const val TAG_ACTION = "action"
345         private const val TAG_INCLUDE = "include"
346 
347         /**  */
348         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
349         public const val APPLICATION_ID_PLACEHOLDER: String = "\${applicationId}"
350         private val sTmpValue = ThreadLocal<TypedValue>()
351 
352         @Throws(XmlPullParserException::class)
353         internal fun checkNavType(
354             value: TypedValue,
355             navType: NavType<*>?,
356             expectedNavType: NavType<*>,
357             argType: String?,
358             foundType: String
359         ): NavType<*> {
360             if (navType != null && navType !== expectedNavType) {
361                 throw XmlPullParserException("Type is $argType but found $foundType: ${value.data}")
362             }
363             return navType ?: expectedNavType
364         }
365     }
366 }
367