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.content.Context;
     20 import android.content.Intent;
     21 import android.content.res.Resources;
     22 import android.content.res.TypedArray;
     23 import android.net.Uri;
     24 import android.os.Bundle;
     25 import android.support.annotation.CallSuper;
     26 import android.support.annotation.IdRes;
     27 import android.support.annotation.NonNull;
     28 import android.support.annotation.Nullable;
     29 import android.support.v4.util.Pair;
     30 import android.support.v4.util.SparseArrayCompat;
     31 import android.util.AttributeSet;
     32 
     33 import androidx.navigation.common.R;
     34 
     35 import java.util.ArrayDeque;
     36 import java.util.ArrayList;
     37 
     38 /**
     39  * NavDestination represents one node within an overall navigation graph.
     40  *
     41  * <p>Each destination is associated with a {@link Navigator} which knows how to navigate to this
     42  * particular destination.</p>
     43  *
     44  * <p>Destinations declare a set of {@link #putAction(int, int) actions} that they
     45  * support. These actions form a navigation API for the destination; the same actions declared
     46  * on different destinations that fill similar roles allow application code to navigate based
     47  * on semantic intent.</p>
     48  *
     49  * <p>Each destination has a set of {@link #getDefaultArguments() default arguments} that will
     50  * be applied when {@link NavController#navigate(int, Bundle) navigating} to that destination.
     51  * These arguments can be overridden at the time of navigation.</p>
     52  */
     53 public class NavDestination {
     54 
     55     /**
     56      * Retrieve a suitable display name for a given id.
     57      * @param context Context used to resolve a resource's name
     58      * @param id The id to get a display name for
     59      * @return The resource's name if it is a valid id or just the id itself if it is not
     60      * a valid resource
     61      */
     62     @NonNull
     63     static String getDisplayName(@NonNull Context context, int id) {
     64         try {
     65             return context.getResources().getResourceName(id);
     66         } catch (Resources.NotFoundException e) {
     67             return Integer.toString(id);
     68         }
     69     }
     70 
     71     private final Navigator mNavigator;
     72     private NavGraph mParent;
     73     private int mId;
     74     private CharSequence mLabel;
     75     private Bundle mDefaultArgs;
     76     private ArrayList<NavDeepLink> mDeepLinks;
     77     private SparseArrayCompat<NavAction> mActions;
     78 
     79     /**
     80      * NavDestinations should be created via {@link Navigator#createDestination}.
     81      */
     82     public NavDestination(@NonNull Navigator<? extends NavDestination> navigator) {
     83         mNavigator = navigator;
     84     }
     85 
     86     /**
     87      * Called when inflating a destination from a resource.
     88      *
     89      * @param context local context performing inflation
     90      * @param attrs attrs to parse during inflation
     91      */
     92     @CallSuper
     93     public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
     94         final TypedArray a = context.getResources().obtainAttributes(attrs,
     95                 R.styleable.Navigator);
     96         setId(a.getResourceId(R.styleable.Navigator_android_id, 0));
     97         setLabel(a.getText(R.styleable.Navigator_android_label));
     98         a.recycle();
     99     }
    100 
    101     void setParent(NavGraph parent) {
    102         mParent = parent;
    103     }
    104 
    105     /**
    106      * Gets the {@link NavGraph} that contains this destination. This will be set when a
    107      * destination is added to a NavGraph via {@link NavGraph#addDestination}.
    108      * @return
    109      */
    110     @Nullable
    111     public NavGraph getParent() {
    112         return mParent;
    113     }
    114 
    115     /**
    116      * Returns the destination's unique ID. This should be an ID resource generated by
    117      * the Android resource system.
    118      *
    119      * @return this destination's ID
    120      */
    121     @IdRes
    122     public int getId() {
    123         return mId;
    124     }
    125 
    126     /**
    127      * Sets the destination's unique ID. This should be an ID resource generated by
    128      * the Android resource system.
    129      *
    130      * @param id this destination's new ID
    131      */
    132     public void setId(@IdRes int id) {
    133         mId = id;
    134     }
    135 
    136     /**
    137      * Sets the descriptive label of this destination.
    138      *
    139      * @param label A descriptive label of this destination.
    140      */
    141     public void setLabel(@Nullable CharSequence label) {
    142         mLabel = label;
    143     }
    144 
    145     /**
    146      * Gets the descriptive label of this destination.
    147      */
    148     @Nullable
    149     public CharSequence getLabel() {
    150         return mLabel;
    151     }
    152 
    153     /**
    154      * Returns the destination's {@link Navigator}.
    155      *
    156      * @return this destination's navigator
    157      */
    158     @NonNull
    159     public Navigator getNavigator() {
    160         return mNavigator;
    161     }
    162 
    163     /**
    164      * Returns the destination's default arguments bundle.
    165      *
    166      * @return the default arguments bundle
    167      */
    168     public @NonNull Bundle getDefaultArguments() {
    169         if (mDefaultArgs == null) {
    170             mDefaultArgs = new Bundle();
    171         }
    172         return mDefaultArgs;
    173     }
    174 
    175     /**
    176      * Sets the destination's default arguments bundle.
    177      *
    178      * @param args the new bundle to set
    179      */
    180     public void setDefaultArguments(@Nullable Bundle args) {
    181         mDefaultArgs = args;
    182     }
    183 
    184     /**
    185      * Merges a bundle of arguments into the current default arguments for this destination.
    186      * New values with the same keys will replace old values with those keys.
    187      *
    188      * @param args arguments to add
    189      */
    190     public void addDefaultArguments(@NonNull Bundle args) {
    191         getDefaultArguments().putAll(args);
    192     }
    193 
    194     /**
    195      * Add a deep link to this destination. Matching Uris sent to
    196      * {@link NavController#onHandleDeepLink(Intent)} will trigger navigating to this destination.
    197      * <p>
    198      * In addition to a direct Uri match, the following features are supported:
    199      * <ul>
    200      *     <li>Uris without a scheme are assumed as http and https. For example,
    201      *     <code>www.example.com</code> will match <code>http://www.example.com</code> and
    202      *     <code>https://www.example.com</code>.</li>
    203      *     <li>Placeholders in the form of <code>{placeholder_name}</code> matches 1 or more
    204      *     characters. The String value of the placeholder will be available in the arguments
    205      *     {@link Bundle} with a key of the same name. For example,
    206      *     <code>http://www.example.com/users/{id}</code> will match
    207      *     <code>http://www.example.com/users/4</code>.</li>
    208      *     <li>The <code>.*</code> wildcard can be used to match 0 or more characters.</li>
    209      * </ul>
    210      * These Uris can be declared in your navigation XML files by adding one or more
    211      * <code>&lt;deepLink app:uri="uriPattern" /&gt;</code> elements as
    212      * a child to your destination.
    213      * <p>
    214      * Deep links added in navigation XML files will automatically replace instances of
    215      * <code>${applicationId}</code> with the applicationId of your app.
    216      * Programmatically added deep links should use {@link Context#getPackageName()} directly
    217      * when constructing the uriPattern.
    218      * @param uriPattern The uri pattern to add as a deep link
    219      * @see NavController#onHandleDeepLink(Intent)
    220      */
    221     public void addDeepLink(@NonNull String uriPattern) {
    222         if (mDeepLinks == null) {
    223             mDeepLinks = new ArrayList<>();
    224         }
    225         mDeepLinks.add(new NavDeepLink(uriPattern));
    226     }
    227 
    228     /**
    229      * Determines if this NavDestination has a deep link matching the given Uri.
    230      * @param uri The Uri to match against all deep links added in {@link #addDeepLink(String)}
    231      * @return The matching {@link NavDestination} and the appropriate {@link Bundle} of arguments
    232      * extracted from the Uri, or null if no match was found.
    233      */
    234     @Nullable
    235     Pair<NavDestination, Bundle> matchDeepLink(@NonNull Uri uri) {
    236         if (mDeepLinks == null) {
    237             return null;
    238         }
    239         for (NavDeepLink deepLink : mDeepLinks) {
    240             Bundle matchingArguments = deepLink.getMatchingArguments(uri);
    241             if (matchingArguments != null) {
    242                 return Pair.create(this, matchingArguments);
    243             }
    244         }
    245         return null;
    246     }
    247 
    248     /**
    249      * Build an array containing the hierarchy from the root down to this destination.
    250      *
    251      * @return An array containing all of the ids from the root to this destination
    252      */
    253     @NonNull
    254     int[] buildDeepLinkIds() {
    255         ArrayDeque<NavDestination> hierarchy = new ArrayDeque<>();
    256         NavDestination current = this;
    257         do {
    258             NavGraph parent = current.getParent();
    259             if (parent == null || parent.getStartDestination() != current.getId()) {
    260                 hierarchy.addFirst(current);
    261             }
    262             current = parent;
    263         } while (current != null);
    264         int[] deepLinkIds = new int[hierarchy.size()];
    265         int index = 0;
    266         for (NavDestination destination : hierarchy) {
    267             deepLinkIds[index++] = destination.getId();
    268         }
    269         return deepLinkIds;
    270     }
    271 
    272     /**
    273      * Returns the destination ID for a given action. This will recursively check the
    274      * {@link #getParent() parent} of this destination if the action destination is not found in
    275      * this destination.
    276      *
    277      * @param id action ID to fetch
    278      * @return destination ID mapped to the given action id, or 0 if none
    279      */
    280     @Nullable
    281     public NavAction getAction(@IdRes int id) {
    282         NavAction destination = mActions == null ? null : mActions.get(id);
    283         // Search the parent for the given action if it is not found in this destination
    284         return destination != null
    285                 ? destination
    286                 : getParent() != null ? getParent().getAction(id) : null;
    287     }
    288 
    289     /**
    290      * Sets a destination ID for an action ID.
    291      *
    292      * @param actionId action ID to bind
    293      * @param destId destination ID for the given action
    294      */
    295     public void putAction(@IdRes int actionId, @IdRes int destId) {
    296         putAction(actionId, new NavAction(destId));
    297     }
    298 
    299     /**
    300      * Sets a destination ID for an action ID.
    301      *
    302      * @param actionId action ID to bind
    303      * @param action action to associate with this action ID
    304      */
    305     public void putAction(@IdRes int actionId, @NonNull NavAction action) {
    306         if (actionId == 0) {
    307             throw new IllegalArgumentException("Cannot have an action with actionId 0");
    308         }
    309         if (mActions == null) {
    310             mActions = new SparseArrayCompat<>();
    311         }
    312         mActions.put(actionId, action);
    313     }
    314 
    315     /**
    316      * Unsets the destination ID for an action ID.
    317      *
    318      * @param actionId action ID to remove
    319      */
    320     public void removeAction(@IdRes int actionId) {
    321         if (mActions == null) {
    322             return;
    323         }
    324         mActions.delete(actionId);
    325     }
    326 
    327     /**
    328      * Navigates to this destination.
    329      *
    330      * <p>Uses the {@link #getNavigator() configured navigator} to navigate to this destination.
    331      * Apps should not call this directly, instead use {@link NavController}'s navigation methods
    332      * to ensure consistent back stack tracking and behavior.</p>
    333      *
    334      * @param args arguments to the new destination
    335      * @param navOptions options for navigation
    336      */
    337     @SuppressWarnings("unchecked")
    338     public void navigate(@Nullable Bundle args, @Nullable NavOptions navOptions) {
    339         Bundle defaultArgs = getDefaultArguments();
    340         Bundle finalArgs = new Bundle();
    341         finalArgs.putAll(defaultArgs);
    342         if (args != null) {
    343             finalArgs.putAll(args);
    344         }
    345         mNavigator.navigate(this, finalArgs, navOptions);
    346     }
    347 }
    348