1 /* 2 * 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 17 package androidx.navigation; 18 19 import android.annotation.SuppressLint; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.content.res.TypedArray; 23 import android.content.res.XmlResourceParser; 24 import android.os.Bundle; 25 import android.support.annotation.NavigationRes; 26 import android.support.annotation.NonNull; 27 import android.support.annotation.Nullable; 28 import android.text.TextUtils; 29 import android.util.AttributeSet; 30 import android.util.TypedValue; 31 import android.util.Xml; 32 33 import org.xmlpull.v1.XmlPullParser; 34 import org.xmlpull.v1.XmlPullParserException; 35 36 import java.io.IOException; 37 38 /** 39 * Class which translates a navigation XML file into a {@link NavGraph} 40 */ 41 public class NavInflater { 42 /** 43 * Metadata key for defining an app's default navigation graph. 44 * 45 * <p>Applications may declare a graph resource in their manifest instead of declaring 46 * or passing this data to each host or controller:</p> 47 * 48 * <pre class="prettyprint"> 49 * <meta-data android:name="android.nav.graph" android:resource="@xml/my_nav_graph" /> 50 * </pre> 51 * 52 * <p>A graph resource declared in this manner can be inflated into a controller by calling 53 * {@link NavController#setMetadataGraph()} or directly via {@link #inflateMetadataGraph()}. 54 * Navigation host implementations should do this automatically 55 * if no navigation resource is otherwise supplied during host configuration.</p> 56 */ 57 @SuppressWarnings("WeakerAccess") 58 public static final String METADATA_KEY_GRAPH = "android.nav.graph"; 59 60 private static final String TAG_ARGUMENT = "argument"; 61 private static final String TAG_DEEP_LINK = "deepLink"; 62 private static final String TAG_ACTION = "action"; 63 private static final String TAG_INCLUDE = "include"; 64 private static final String APPLICATION_ID_PLACEHOLDER = "${applicationId}"; 65 66 private static final ThreadLocal<TypedValue> sTmpValue = new ThreadLocal<>(); 67 68 private Context mContext; 69 private NavigatorProvider mNavigatorProvider; 70 71 public NavInflater(@NonNull Context context, @NonNull NavigatorProvider navigatorProvider) { 72 mContext = context; 73 mNavigatorProvider = navigatorProvider; 74 } 75 76 /** 77 * Inflates {@link NavGraph navigation graph} as specified in the application manifest. 78 * 79 * <p>Applications may declare a graph resource in their manifest instead of declaring 80 * or passing this data to each host or controller:</p> 81 * 82 * <pre class="prettyprint"> 83 * <meta-data android:name="android.nav.graph" android:resource="@xml/my_nav_graph" /> 84 * </pre> 85 * 86 * @see #METADATA_KEY_GRAPH 87 */ 88 @Nullable 89 public NavGraph inflateMetadataGraph() { 90 final Bundle metaData = mContext.getApplicationInfo().metaData; 91 if (metaData != null) { 92 final int resid = metaData.getInt(METADATA_KEY_GRAPH); 93 if (resid != 0) { 94 return inflate(resid); 95 } 96 } 97 return null; 98 } 99 /** 100 * Inflate a NavGraph from the given XML resource id. 101 * 102 * @param graphResId 103 * @return 104 */ 105 @SuppressLint("ResourceType") 106 @NonNull 107 public NavGraph inflate(@NavigationRes int graphResId) { 108 Resources res = mContext.getResources(); 109 XmlResourceParser parser = res.getXml(graphResId); 110 final AttributeSet attrs = Xml.asAttributeSet(parser); 111 try { 112 int type; 113 while ((type = parser.next()) != XmlPullParser.START_TAG 114 && type != XmlPullParser.END_DOCUMENT) { 115 // Empty loop 116 } 117 if (type != XmlPullParser.START_TAG) { 118 throw new XmlPullParserException("No start tag found"); 119 } 120 121 String rootElement = parser.getName(); 122 NavDestination destination = inflate(res, parser, attrs); 123 if (!(destination instanceof NavGraph)) { 124 throw new IllegalArgumentException("Root element <" + rootElement + ">" 125 + " did not inflate into a NavGraph"); 126 } 127 return (NavGraph) destination; 128 } catch (Exception e) { 129 throw new RuntimeException("Exception inflating " 130 + res.getResourceName(graphResId) + " line " 131 + parser.getLineNumber(), e); 132 } finally { 133 parser.close(); 134 } 135 } 136 137 @NonNull 138 private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser, 139 @NonNull AttributeSet attrs) throws XmlPullParserException, IOException { 140 Navigator navigator = mNavigatorProvider.getNavigator(parser.getName()); 141 final NavDestination dest = navigator.createDestination(); 142 143 dest.onInflate(mContext, attrs); 144 145 final int innerDepth = parser.getDepth() + 1; 146 int type; 147 int depth; 148 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 149 && ((depth = parser.getDepth()) >= innerDepth 150 || type != XmlPullParser.END_TAG)) { 151 if (type != XmlPullParser.START_TAG) { 152 continue; 153 } 154 155 if (depth > innerDepth) { 156 continue; 157 } 158 159 final String name = parser.getName(); 160 if (TAG_ARGUMENT.equals(name)) { 161 inflateArgument(res, dest, attrs); 162 } else if (TAG_DEEP_LINK.equals(name)) { 163 inflateDeepLink(res, dest, attrs); 164 } else if (TAG_ACTION.equals(name)) { 165 inflateAction(res, dest, attrs); 166 } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) { 167 final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude); 168 final int id = a.getResourceId(R.styleable.NavInclude_graph, 0); 169 ((NavGraph) dest).addDestination(inflate(id)); 170 a.recycle(); 171 } else if (dest instanceof NavGraph) { 172 ((NavGraph) dest).addDestination(inflate(res, parser, attrs)); 173 } 174 } 175 176 return dest; 177 } 178 179 private void inflateArgument(@NonNull Resources res, @NonNull NavDestination dest, 180 @NonNull AttributeSet attrs) throws XmlPullParserException { 181 final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavArgument); 182 String name = a.getString(R.styleable.NavArgument_android_name); 183 184 TypedValue value = sTmpValue.get(); 185 if (value == null) { 186 value = new TypedValue(); 187 sTmpValue.set(value); 188 } 189 if (a.getValue(R.styleable.NavArgument_android_defaultValue, value)) { 190 switch (value.type) { 191 case TypedValue.TYPE_STRING: 192 dest.getDefaultArguments().putString(name, value.string.toString()); 193 break; 194 case TypedValue.TYPE_DIMENSION: 195 dest.getDefaultArguments().putInt(name, 196 (int) value.getDimension(res.getDisplayMetrics())); 197 break; 198 case TypedValue.TYPE_FLOAT: 199 dest.getDefaultArguments().putFloat(name, value.getFloat()); 200 break; 201 case TypedValue.TYPE_REFERENCE: 202 dest.getDefaultArguments().putInt(name, value.data); 203 break; 204 default: 205 if (value.type >= TypedValue.TYPE_FIRST_INT 206 && value.type <= TypedValue.TYPE_LAST_INT) { 207 dest.getDefaultArguments().putInt(name, value.data); 208 } else { 209 throw new XmlPullParserException("unsupported argument type " + value.type); 210 } 211 } 212 } 213 a.recycle(); 214 } 215 216 private void inflateDeepLink(@NonNull Resources res, @NonNull NavDestination dest, 217 @NonNull AttributeSet attrs) { 218 final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavDeepLink); 219 String uri = a.getString(R.styleable.NavDeepLink_uri); 220 if (TextUtils.isEmpty(uri)) { 221 throw new IllegalArgumentException("Every <" + TAG_DEEP_LINK 222 + "> must include an app:uri"); 223 } 224 uri = uri.replace(APPLICATION_ID_PLACEHOLDER, mContext.getPackageName()); 225 dest.addDeepLink(uri); 226 a.recycle(); 227 } 228 229 @SuppressWarnings("deprecation") 230 private void inflateAction(@NonNull Resources res, @NonNull NavDestination dest, 231 @NonNull AttributeSet attrs) { 232 final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavAction); 233 final int id = a.getResourceId(R.styleable.NavAction_android_id, 0); 234 final int destId = a.getResourceId(R.styleable.NavAction_destination, 0); 235 NavAction action = new NavAction(destId); 236 237 NavOptions.Builder builder = new NavOptions.Builder(); 238 builder.setLaunchSingleTop(a.getBoolean(R.styleable.NavAction_launchSingleTop, false)); 239 builder.setLaunchDocument(a.getBoolean(R.styleable.NavAction_launchDocument, false)); 240 builder.setClearTask(a.getBoolean(R.styleable.NavAction_clearTask, false)); 241 builder.setPopUpTo(a.getResourceId(R.styleable.NavAction_popUpTo, 0), 242 a.getBoolean(R.styleable.NavAction_popUpToInclusive, false)); 243 builder.setEnterAnim(a.getResourceId(R.styleable.NavAction_enterAnim, -1)); 244 builder.setExitAnim(a.getResourceId(R.styleable.NavAction_exitAnim, -1)); 245 builder.setPopEnterAnim(a.getResourceId(R.styleable.NavAction_popEnterAnim, -1)); 246 builder.setPopExitAnim(a.getResourceId(R.styleable.NavAction_popExitAnim, -1)); 247 action.setNavOptions(builder.build()); 248 249 dest.putAction(id, action); 250 a.recycle(); 251 } 252 } 253