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
17package androidx.navigation;
18
19import android.app.Activity;
20import android.content.Context;
21import android.content.ContextWrapper;
22import android.content.Intent;
23import android.os.Bundle;
24import android.support.annotation.IdRes;
25import android.support.annotation.NavigationRes;
26import android.support.annotation.NonNull;
27import android.support.annotation.Nullable;
28import android.support.v4.app.TaskStackBuilder;
29import android.support.v4.util.Pair;
30
31import java.util.ArrayDeque;
32import java.util.ArrayList;
33import java.util.Deque;
34import java.util.Iterator;
35import java.util.Map;
36import java.util.concurrent.CopyOnWriteArrayList;
37
38/**
39 * NavController manages app navigation within a {@link NavHost}.
40 *
41 * <p>Apps will generally obtain a controller directly from a host, or by using one of the utility
42 * methods on the {@link Navigation} class rather than create a controller directly.</p>
43 *
44 * <p>Navigation flows and destinations are determined by the
45 * {@link NavGraph navigation graph} owned by the controller. These graphs are typically
46 * {@link #getNavInflater() inflated} from an Android resource, but, like views, they can also
47 * be constructed or combined programmatically or for the case of dynamic navigation structure.
48 * (For example, if the navigation structure of the application is determined by live data obtained'
49 * from a remote server.)</p>
50 */
51public class NavController {
52    private static final String KEY_NAVIGATOR_STATE =
53            "android-support-nav:controller:navigatorState";
54    private static final String KEY_NAVIGATOR_STATE_NAMES =
55            "android-support-nav:controller:navigatorState:names";
56    private static final String KEY_GRAPH_ID = "android-support-nav:controller:graphId";
57    private static final String KEY_BACK_STACK_IDS = "android-support-nav:controller:backStackIds";
58    static final String KEY_DEEP_LINK_IDS = "android-support-nav:controller:deepLinkIds";
59    static final String KEY_DEEP_LINK_EXTRAS =
60            "android-support-nav:controller:deepLinkExtras";
61    /**
62     * The {@link Intent} that triggered a deep link to the current destination.
63     */
64    public static final String KEY_DEEP_LINK_INTENT =
65            "android-support-nav:controller:deepLinkIntent";
66
67    private final Context mContext;
68    private Activity mActivity;
69    private NavInflater mInflater;
70    private NavGraph mGraph;
71    private int mGraphId;
72    private Bundle mNavigatorStateToRestore;
73    private int[] mBackStackToRestore;
74
75    private final Deque<NavDestination> mBackStack = new ArrayDeque<>();
76
77    private final SimpleNavigatorProvider mNavigatorProvider = new SimpleNavigatorProvider() {
78        @Nullable
79        @Override
80        public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
81                @NonNull Navigator<? extends NavDestination> navigator) {
82            Navigator<? extends NavDestination> previousNavigator =
83                    super.addNavigator(name, navigator);
84            if (previousNavigator != navigator) {
85                if (previousNavigator != null) {
86                    previousNavigator.removeOnNavigatorNavigatedListener(mOnNavigatedListener);
87                }
88                navigator.addOnNavigatorNavigatedListener(mOnNavigatedListener);
89            }
90            return previousNavigator;
91        }
92    };
93
94    private final Navigator.OnNavigatorNavigatedListener mOnNavigatedListener =
95            new Navigator.OnNavigatorNavigatedListener() {
96                @Override
97                public void onNavigatorNavigated(@NonNull Navigator navigator, @IdRes int destId,
98                        @Navigator.BackStackEffect int backStackEffect) {
99                    if (destId != 0) {
100                        // First remove popped destinations off the back stack
101                        if (backStackEffect == Navigator.BACK_STACK_DESTINATION_POPPED) {
102                            while (!mBackStack.isEmpty()
103                                    && mBackStack.peekLast().getId() != destId) {
104                                mBackStack.removeLast();
105                            }
106                        }
107                        NavDestination newDest = findDestination(destId);
108                        if (newDest == null) {
109                            throw new IllegalArgumentException("Navigator " + navigator
110                                    + " reported navigation to unknown destination id "
111                                    + NavDestination.getDisplayName(mContext, destId));
112                        }
113                        if (backStackEffect == Navigator.BACK_STACK_DESTINATION_ADDED) {
114                            // Add the new destination to the back stack
115                            mBackStack.add(newDest);
116                        }
117                        // Don't dispatchOnNavigated if nothing changed
118                        if (backStackEffect != Navigator.BACK_STACK_UNCHANGED) {
119                            dispatchOnNavigated(newDest);
120                        }
121                    }
122                }
123            };
124
125    private final CopyOnWriteArrayList<OnNavigatedListener> mOnNavigatedListeners =
126            new CopyOnWriteArrayList<>();
127
128    /**
129     * OnNavigatorNavigatedListener receives a callback when the associated controller
130     * navigates to a new destination.
131     */
132    public interface OnNavigatedListener {
133        /**
134         * onNavigatorNavigated is called when the controller navigates to a new destination.
135         * This navigation may be to a destination that has not been seen before, or one that
136         * was previously on the back stack. This method is called after navigation is complete,
137         * but associated transitions may still be playing.
138         *
139         * @param controller the controller that navigated
140         * @param destination the new destination
141         */
142        void onNavigated(@NonNull NavController controller, @NonNull NavDestination destination);
143    }
144
145    /**
146     * Constructs a new controller for a given {@link Context}. Controllers should not be
147     * used outside of their context and retain a hard reference to the context supplied.
148     * If you need a global controller, pass {@link Context#getApplicationContext()}.
149     *
150     * <p>Apps should generally not construct controllers, instead obtain a relevant controller
151     * directly from a navigation host via {@link NavHost#getNavController()} or by using one of
152     * the utility methods on the {@link Navigation} class.</p>
153     *
154     * <p>Note that controllers that are not constructed with an {@link Activity} context
155     * (or a wrapped activity context) will only be able to navigate to
156     * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK new tasks} or
157     * {@link android.content.Intent#FLAG_ACTIVITY_NEW_DOCUMENT new document tasks} when
158     * navigating to new activities.</p>
159     *
160     * @param context context for this controller
161     */
162    public NavController(@NonNull Context context) {
163        mContext = context;
164        while (context instanceof ContextWrapper) {
165            if (context instanceof Activity) {
166                mActivity = (Activity) context;
167                break;
168            }
169            context = ((ContextWrapper) context).getBaseContext();
170        }
171        mNavigatorProvider.addNavigator(new NavGraphNavigator(mContext));
172        mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
173    }
174
175    @NonNull
176    Context getContext() {
177        return mContext;
178    }
179
180    /**
181     * Retrieve the NavController's {@link NavigatorProvider}. All {@link Navigator Navigators} used
182     * to construct the {@link NavGraph navigation graph} for this nav controller should be added
183     * to this navigator provider before the graph is constructed.
184     * <p>
185     * Generally, the Navigators are set for you by the {@link NavHost} hosting this NavController
186     * and you do not need to manually interact with the navigator provider.
187     * </p>
188     * @return The {@link NavigatorProvider} used by this NavController.
189     */
190    @NonNull
191    public NavigatorProvider getNavigatorProvider() {
192        return mNavigatorProvider;
193    }
194
195    /**
196     * Adds an {@link OnNavigatedListener} to this controller to receive events when
197     * the controller navigates to a new destination.
198     *
199     * <p>The current destination, if any, will be immediately sent to your listener.</p>
200     *
201     * @param listener the listener to receive events
202     */
203    public void addOnNavigatedListener(@NonNull OnNavigatedListener listener) {
204        // Inform the new listener of our current state, if any
205        if (!mBackStack.isEmpty()) {
206            listener.onNavigated(this, mBackStack.peekLast());
207        }
208        mOnNavigatedListeners.add(listener);
209    }
210
211    /**
212     * Removes an {@link OnNavigatedListener} from this controller. It will no longer
213     * receive navigation events.
214     *
215     * @param listener the listener to remove
216     */
217    public void removeOnNavigatedListener(@NonNull OnNavigatedListener listener) {
218        mOnNavigatedListeners.remove(listener);
219    }
220
221    /**
222     * Attempts to pop the controller's back stack. Analogous to when the user presses
223     * the system {@link android.view.KeyEvent#KEYCODE_BACK Back} button when the associated
224     * navigation host has focus.
225     *
226     * @return true if the stack was popped, false otherwise
227     */
228    public boolean popBackStack() {
229        if (mBackStack.isEmpty()) {
230            throw new IllegalArgumentException("NavController back stack is empty");
231        }
232        boolean popped = false;
233        while (!mBackStack.isEmpty()) {
234            popped = mBackStack.removeLast().getNavigator().popBackStack();
235            if (popped) {
236                break;
237            }
238        }
239        return popped;
240    }
241
242
243    /**
244     * Attempts to pop the controller's back stack back to a specific destination.
245     *
246     * @param destinationId The topmost destination to retain
247     * @param inclusive Whether the given destination should also be popped.
248     *
249     * @return true if the stack was popped at least once, false otherwise
250     */
251    public boolean popBackStack(@IdRes int destinationId, boolean inclusive) {
252        if (mBackStack.isEmpty()) {
253            throw new IllegalArgumentException("NavController back stack is empty");
254        }
255        ArrayList<NavDestination> destinationsToRemove = new ArrayList<>();
256        Iterator<NavDestination> iterator = mBackStack.descendingIterator();
257        while (iterator.hasNext()) {
258            NavDestination destination = iterator.next();
259            if (inclusive || destination.getId() != destinationId) {
260                destinationsToRemove.add(destination);
261            }
262            if (destination.getId() == destinationId) {
263                break;
264            }
265        }
266        boolean popped = false;
267        iterator = destinationsToRemove.iterator();
268        while (iterator.hasNext()) {
269            NavDestination destination = iterator.next();
270            // Skip destinations already removed by a previous popBackStack operation
271            while (!mBackStack.isEmpty() && mBackStack.peekLast().getId() != destination.getId()) {
272                if (iterator.hasNext()) {
273                    destination = iterator.next();
274                } else {
275                    destination = null;
276                    break;
277                }
278            }
279            if (destination != null) {
280                popped = destination.getNavigator().popBackStack() || popped;
281            }
282        }
283        return popped;
284    }
285
286    /**
287     * Attempts to navigate up in the navigation hierarchy. Suitable for when the
288     * user presses the "Up" button marked with a left (or start)-facing arrow in the upper left
289     * (or starting) corner of the app UI.
290     *
291     * <p>The intended behavior of Up differs from {@link #popBackStack() Back} when the user
292     * did not reach the current destination from the application's own task. e.g. if the user
293     * is viewing a document or link in the current app in an activity hosted on another app's
294     * task where the user clicked the link. In this case the current activity (determined by the
295     * context used to create this NavController) will be {@link Activity#finish() finished} and
296     * the user will be taken to an appropriate destination in this app on its own task.</p>
297     *
298     * @return true if navigation was successful, false otherwise
299     */
300    public boolean navigateUp() {
301        if (mBackStack.size() == 1) {
302            // If there's only one entry, then we've deep linked into a specific destination
303            // on another task so we need to find the parent and start our task from there
304            NavDestination currentDestination = getCurrentDestination();
305            int destId = currentDestination.getId();
306            NavGraph parent = currentDestination.getParent();
307            while (parent != null) {
308                if (parent.getStartDestination() != destId) {
309                    TaskStackBuilder parentIntents = new NavDeepLinkBuilder(NavController.this)
310                            .setDestination(parent.getId())
311                            .createTaskStackBuilder();
312                    parentIntents.startActivities();
313                    if (mActivity != null) {
314                        mActivity.finish();
315                    }
316                    return true;
317                }
318                destId = parent.getId();
319                parent = parent.getParent();
320            }
321            // We're already at the startDestination of the graph so there's no 'Up' to go to
322            return false;
323        } else {
324            return popBackStack();
325        }
326    }
327
328    private void dispatchOnNavigated(NavDestination destination) {
329        for (OnNavigatedListener listener : mOnNavigatedListeners) {
330            listener.onNavigated(this, destination);
331        }
332    }
333
334    /**
335     * Sets the {@link NavGraph navigation graph} as specified in the application manifest.
336     *
337     * <p>Applications may declare a graph resource in their manifest instead of declaring
338     * or passing this data to each host or controller:</p>
339     *
340     * <pre class="prettyprint">
341     *     <meta-data android:name="android.nav.graph" android:resource="@xml/my_nav_graph" />
342     * </pre>
343     *
344     * <p>The inflated graph can be retrieved via {@link #getGraph()}. Calling this will have no
345     * effect if there is no metadata graph specified.</p>
346     *
347     * @see NavInflater#METADATA_KEY_GRAPH
348     * @see NavInflater#inflateMetadataGraph()
349     * @see #getGraph
350     */
351    public void setMetadataGraph() {
352        NavGraph metadataGraph = getNavInflater().inflateMetadataGraph();
353        if (metadataGraph != null) {
354            setGraph(metadataGraph);
355        }
356    }
357
358    /**
359     * Returns the {@link NavInflater inflater} for this controller.
360     *
361     * @return inflater for loading navigation resources
362     */
363    @NonNull
364    public NavInflater getNavInflater() {
365        if (mInflater == null) {
366            mInflater = new NavInflater(mContext, mNavigatorProvider);
367        }
368        return mInflater;
369    }
370
371    /**
372     * Sets the {@link NavGraph navigation graph} to the specified resource.
373     * Any current navigation graph data will be replaced.
374     *
375     * <p>The inflated graph can be retrieved via {@link #getGraph()}.</p>
376     *
377     * @param graphResId resource id of the navigation graph to inflate
378     *
379     * @see #getNavInflater()
380     * @see #setGraph(NavGraph)
381     * @see #getGraph
382     */
383    public void setGraph(@NavigationRes int graphResId) {
384        mGraph = getNavInflater().inflate(graphResId);
385        mGraphId = graphResId;
386        onGraphCreated();
387    }
388
389    /**
390     * Sets the {@link NavGraph navigation graph} to the specified graph.
391     * Any current navigation graph data will be replaced.
392     *
393     * <p>The graph can be retrieved later via {@link #getGraph()}.</p>
394     *
395     * @param graph graph to set
396     * @see #setGraph(int)
397     * @see #getGraph
398     */
399    public void setGraph(@NonNull NavGraph graph) {
400        mGraph = graph;
401        mGraphId = 0;
402        onGraphCreated();
403    }
404
405    private void onGraphCreated() {
406        if (mNavigatorStateToRestore != null) {
407            ArrayList<String> navigatorNames = mNavigatorStateToRestore.getStringArrayList(
408                    KEY_NAVIGATOR_STATE_NAMES);
409            if (navigatorNames != null) {
410                for (String name : navigatorNames) {
411                    Navigator navigator = mNavigatorProvider.getNavigator(name);
412                    Bundle bundle = mNavigatorStateToRestore.getBundle(name);
413                    if (bundle != null) {
414                        navigator.onRestoreState(bundle);
415                    }
416                }
417            }
418        }
419        if (mBackStackToRestore != null) {
420            for (int destinationId : mBackStackToRestore) {
421                NavDestination node = findDestination(destinationId);
422                if (node == null) {
423                    throw new IllegalStateException("unknown destination during restore: "
424                            + mContext.getResources().getResourceName(destinationId));
425                }
426                mBackStack.add(node);
427            }
428            mBackStackToRestore = null;
429        }
430        if (mGraph != null && mBackStack.isEmpty()) {
431            boolean deepLinked = mActivity != null && onHandleDeepLink(mActivity.getIntent());
432            if (!deepLinked) {
433                // Navigate to the first destination in the graph
434                // if we haven't deep linked to a destination
435                mGraph.navigate(null, null);
436            }
437        }
438    }
439
440    /**
441     * Checks the given Intent for a Navigation deep link and navigates to the deep link if present.
442     * This is called automatically for you the first time you set the graph if you've passed in an
443     * {@link Activity} as the context when constructing this NavController, but should be manually
444     * called if your Activity receives new Intents in {@link Activity#onNewIntent(Intent)}.
445     * <p>
446     * The types of Intents that are supported include:
447     * <ul>
448     *     <ol>Intents created by {@link NavDeepLinkBuilder} or
449     *     {@link #createDeepLink()}. This assumes that the current graph shares
450     *     the same hierarchy to get to the deep linked destination as when the deep link was
451     *     constructed.</ol>
452     *     <ol>Intents that include a {@link Intent#getData() data Uri}. This Uri will be checked
453     *     against the Uri patterns added via {@link NavDestination#addDeepLink(String)}.</ol>
454     * </ul>
455     * <p>The {@link #getGraph() navigation graph} should be set before calling this method.</p>
456     * @param intent The Intent that may contain a valid deep link
457     * @return True if the navigation controller found a valid deep link and navigated to it.
458     * @see NavDestination#addDeepLink(String)
459     */
460    public boolean onHandleDeepLink(@Nullable Intent intent) {
461        if (intent == null) {
462            return false;
463        }
464        Bundle extras = intent.getExtras();
465        int[] deepLink = extras != null ? extras.getIntArray(KEY_DEEP_LINK_IDS) : null;
466        Bundle bundle = new Bundle();
467        Bundle deepLinkExtras = extras != null ? extras.getBundle(KEY_DEEP_LINK_EXTRAS) : null;
468        if (deepLinkExtras != null) {
469            bundle.putAll(deepLinkExtras);
470        }
471        if ((deepLink == null || deepLink.length == 0) && intent.getData() != null) {
472            Pair<NavDestination, Bundle> matchingDeepLink = mGraph.matchDeepLink(intent.getData());
473            if (matchingDeepLink != null) {
474                deepLink = matchingDeepLink.first.buildDeepLinkIds();
475                bundle.putAll(matchingDeepLink.second);
476            }
477        }
478        if (deepLink == null || deepLink.length == 0) {
479            return false;
480        }
481        bundle.putParcelable(KEY_DEEP_LINK_INTENT, intent);
482        int flags = intent.getFlags();
483        if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0
484                && (flags & Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0) {
485            // Someone called us with NEW_TASK, but we don't know what state our whole
486            // task stack is in, so we need to manually restart the whole stack to
487            // ensure we're in a predictably good state.
488            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
489            TaskStackBuilder taskStackBuilder = TaskStackBuilder
490                    .create(mContext)
491                    .addNextIntentWithParentStack(intent);
492            taskStackBuilder.startActivities();
493            if (mActivity != null) {
494                mActivity.finish();
495            }
496            return true;
497        }
498        if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
499            // Start with a cleared task starting at our root when we're on our own task
500            if (!mBackStack.isEmpty()) {
501                navigate(mGraph.getStartDestination(), bundle, new NavOptions.Builder()
502                        .setPopUpTo(mGraph.getId(), true)
503                        .setEnterAnim(0).setExitAnim(0).build());
504            }
505            int index = 0;
506            while (index < deepLink.length) {
507                int destinationId = deepLink[index++];
508                NavDestination node = findDestination(destinationId);
509                if (node == null) {
510                    throw new IllegalStateException("unknown destination during deep link: "
511                            + NavDestination.getDisplayName(mContext, destinationId));
512                }
513                node.navigate(bundle,
514                        new NavOptions.Builder().setEnterAnim(0).setExitAnim(0).build());
515            }
516            return true;
517        }
518        // Assume we're on another apps' task and only start the final destination
519        NavGraph graph = mGraph;
520        for (int i = 0; i < deepLink.length; i++) {
521            int destinationId = deepLink[i];
522            NavDestination node = i == 0 ? mGraph : graph.findNode(destinationId);
523            if (node == null) {
524                throw new IllegalStateException("unknown destination during deep link: "
525                        + NavDestination.getDisplayName(mContext, destinationId));
526            }
527            if (i != deepLink.length - 1) {
528                // We're not at the final NavDestination yet, so keep going through the chain
529                graph = (NavGraph) node;
530            } else {
531                // Navigate to the last NavDestination, clearing any existing destinations
532                node.navigate(bundle, new NavOptions.Builder()
533                        .setPopUpTo(mGraph.getId(), true)
534                        .setEnterAnim(0).setExitAnim(0).build());
535            }
536        }
537        return true;
538    }
539
540    /**
541     * Gets the topmost navigation graph associated with this NavController.
542     *
543     * @see #setGraph(int)
544     * @see #setGraph(NavGraph)
545     * @see #setMetadataGraph()
546     */
547    public NavGraph getGraph() {
548        return mGraph;
549    }
550
551    /**
552     * Gets the current destination.
553     */
554    public NavDestination getCurrentDestination() {
555        return mBackStack.peekLast();
556    }
557
558    private NavDestination findDestination(@IdRes int destinationId) {
559        if (mGraph == null) {
560            return null;
561        }
562        if (mGraph.getId() == destinationId) {
563            return mGraph;
564        }
565        NavDestination currentNode = mBackStack.isEmpty() ? mGraph : mBackStack.peekLast();
566        NavGraph currentGraph = currentNode instanceof NavGraph
567                ? (NavGraph) currentNode
568                : currentNode.getParent();
569        return currentGraph.findNode(destinationId);
570    }
571
572    /**
573     * Navigate to a destination from the current navigation graph. This supports both navigating
574     * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
575     *
576     * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
577     *              navigate to
578     */
579    public final void navigate(@IdRes int resId) {
580        navigate(resId, null);
581    }
582
583    /**
584     * Navigate to a destination from the current navigation graph. This supports both navigating
585     * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
586     *
587     * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
588     *              navigate to
589     * @param args arguments to pass to the destination
590     */
591    public final void navigate(@IdRes int resId, @Nullable Bundle args) {
592        navigate(resId, args, null);
593    }
594
595    /**
596     * Navigate to a destination from the current navigation graph. This supports both navigating
597     * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
598     *
599     * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
600     *              navigate to
601     * @param args arguments to pass to the destination
602     * @param navOptions special options for this navigation operation
603     */
604    @SuppressWarnings("deprecation")
605    public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions) {
606        NavDestination currentNode = mBackStack.isEmpty() ? mGraph : mBackStack.peekLast();
607        if (currentNode == null) {
608            throw new IllegalStateException("no current navigation node");
609        }
610        @IdRes int destId = resId;
611        final NavAction navAction = currentNode.getAction(resId);
612        if (navAction != null) {
613            if (navOptions == null) {
614                navOptions = navAction.getNavOptions();
615            }
616            destId = navAction.getDestinationId();
617        }
618        if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != 0) {
619            popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
620            return;
621        }
622
623        if (destId == 0) {
624            throw new IllegalArgumentException("Destination id == 0 can only be used"
625                    + " in conjunction with navOptions.popUpTo != 0");
626        }
627
628        NavDestination node = findDestination(destId);
629        if (node == null) {
630            final String dest = NavDestination.getDisplayName(mContext, destId);
631            throw new IllegalArgumentException("navigation destination " + dest
632                    + (navAction != null
633                    ? " referenced from action " + NavDestination.getDisplayName(mContext, resId)
634                    : "")
635                    + " is unknown to this NavController");
636        }
637        if (navOptions != null) {
638            if (navOptions.shouldClearTask()) {
639                // Start with a clean slate
640                popBackStack(mGraph.getId(), true);
641            } else if (navOptions.getPopUpTo() != 0) {
642                popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
643            }
644        }
645        node.navigate(args, navOptions);
646    }
647
648    /**
649     * Navigate via the given {@link NavDirections}
650     *
651     * @param directions directions that describe this navigation operation
652     */
653    public void navigate(@NonNull NavDirections directions) {
654        navigate(directions.getActionId(), directions.getArguments());
655    }
656
657    /**
658     * Navigate via the given {@link NavDirections}
659     *
660     * @param directions directions that describe this navigation operation
661     */
662    public void navigate(@NonNull NavDirections directions, @Nullable NavOptions navOptions) {
663        navigate(directions.getActionId(), directions.getArguments(), navOptions);
664    }
665    /**
666     * Create a deep link to a destination within this NavController.
667     *
668     * @return a {@link NavDeepLinkBuilder} suitable for constructing a deep link
669     */
670    @NonNull
671    public NavDeepLinkBuilder createDeepLink() {
672        return new NavDeepLinkBuilder(this);
673    }
674
675    /**
676     * Saves all navigation controller state to a Bundle.
677     *
678     * <p>State may be restored from a bundle returned from this method by calling
679     * {@link #restoreState(Bundle)}. Saving controller state is the responsibility
680     * of a {@link NavHost}.</p>
681     *
682     * @return saved state for this controller
683     */
684    @Nullable
685    public Bundle saveState() {
686        Bundle b = null;
687        if (mGraphId != 0) {
688            b = new Bundle();
689            b.putInt(KEY_GRAPH_ID, mGraphId);
690        }
691        ArrayList<String> navigatorNames = new ArrayList<>();
692        Bundle navigatorState = new Bundle();
693        for (Map.Entry<String, Navigator<? extends NavDestination>> entry :
694                mNavigatorProvider.getNavigators().entrySet()) {
695            String name = entry.getKey();
696            Bundle savedState = entry.getValue().onSaveState();
697            if (savedState != null) {
698                navigatorNames.add(name);
699                navigatorState.putBundle(name, entry.getValue().onSaveState());
700            }
701        }
702        if (!navigatorNames.isEmpty()) {
703            if (b == null) {
704                b = new Bundle();
705            }
706            navigatorState.putStringArrayList(KEY_NAVIGATOR_STATE_NAMES, navigatorNames);
707            b.putBundle(KEY_NAVIGATOR_STATE, navigatorState);
708        }
709        if (!mBackStack.isEmpty()) {
710            if (b == null) {
711                b = new Bundle();
712            }
713            int[] backStack = new int[mBackStack.size()];
714            int index = 0;
715            for (NavDestination destination : mBackStack) {
716                backStack[index++] = destination.getId();
717            }
718            b.putIntArray(KEY_BACK_STACK_IDS, backStack);
719        }
720        return b;
721    }
722
723    /**
724     * Restores all navigation controller state from a bundle.
725     *
726     * <p>State may be saved to a bundle by calling {@link #saveState()}.
727     * Restoring controller state is the responsibility of a {@link NavHost}.</p>
728     *
729     * @param navState state bundle to restore
730     */
731    public void restoreState(@Nullable Bundle navState) {
732        if (navState == null) {
733            return;
734        }
735
736        mGraphId = navState.getInt(KEY_GRAPH_ID);
737        mNavigatorStateToRestore = navState.getBundle(KEY_NAVIGATOR_STATE);
738        mBackStackToRestore = navState.getIntArray(KEY_BACK_STACK_IDS);
739        if (mGraphId != 0) {
740            // Set the graph right away, onGraphCreated will handle restoring the
741            // rest of the saved state
742            setGraph(mGraphId);
743        }
744    }
745}
746