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