Home | History | Annotate | Download | only in navigation
      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