public class

NavController

extends java.lang.Object

 java.lang.Object

↳androidx.navigation.NavController

Subclasses:

NavHostController

Overview

NavController manages app navigation within a NavHost.

Apps will generally obtain a controller directly from a host, or by using one of the utility methods on the Navigation class rather than create a controller directly.

Navigation flows and destinations are determined by the navigation graph owned by the controller. These graphs are typically inflated from an Android resource, but, like views, they can also be constructed or combined programmatically or for the case of dynamic navigation structure. (For example, if the navigation structure of the application is determined by live data obtained' from a remote server.)

Summary

Fields
public static final java.lang.StringKEY_DEEP_LINK_INTENT

The that triggered a deep link to the current destination.

Constructors
publicNavController(Context context)

Constructs a new controller for a given .

Methods
public voidaddOnDestinationChangedListener(NavController.OnDestinationChangedListener listener)

Adds an NavController.OnDestinationChangedListener to this controller to receive a callback whenever the NavController.getCurrentDestination() or its arguments change.

public NavDeepLinkBuildercreateDeepLink()

Create a deep link to a destination within this NavController.

public java.util.Deque<NavBackStackEntry>getBackStack()

Retrieve the current back stack.

public NavBackStackEntrygetBackStackEntry(int destinationId)

Gets the topmost NavBackStackEntry for a destination id.

public NavBackStackEntrygetCurrentBackStackEntry()

Gets the topmost NavBackStackEntry.

public NavDestinationgetCurrentDestination()

Gets the current destination.

public NavGraphgetGraph()

Gets the topmost navigation graph associated with this NavController.

public NavigatorProvidergetNavigatorProvider()

Retrieve the NavController's NavigatorProvider.

public NavInflatergetNavInflater()

Returns the inflater for this controller.

public NavBackStackEntrygetPreviousBackStackEntry()

Gets the previous visible NavBackStackEntry.

public ViewModelStoreOwnergetViewModelStoreOwner(int navGraphId)

Gets the ViewModelStoreOwner for a NavGraph.

public booleanhandleDeepLink(Intent intent)

Checks the given Intent for a Navigation deep link and navigates to the deep link if present.

public voidnavigate(int resId)

Navigate to a destination from the current navigation graph.

public voidnavigate(int resId, Bundle args)

Navigate to a destination from the current navigation graph.

public voidnavigate(int resId, Bundle args, NavOptions navOptions)

Navigate to a destination from the current navigation graph.

public voidnavigate(int resId, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras)

Navigate to a destination from the current navigation graph.

public voidnavigate(NavDirections directions, Navigator.Extras navigatorExtras)

Navigate via the given NavDirections

public voidnavigate(Uri deepLink)

Navigate to a destination via the given deep link .

public voidnavigate(Uri deepLink, NavOptions navOptions)

Navigate to a destination via the given deep link .

public voidnavigate(Uri deepLink, NavOptions navOptions, Navigator.Extras navigatorExtras)

Navigate to a destination via the given deep link .

public booleannavigateUp()

Attempts to navigate up in the navigation hierarchy.

public booleanpopBackStack()

Attempts to pop the controller's back stack.

public booleanpopBackStack(int destinationId, boolean inclusive)

Attempts to pop the controller's back stack back to a specific destination.

public voidremoveOnDestinationChangedListener(NavController.OnDestinationChangedListener listener)

Removes an NavController.OnDestinationChangedListener from this controller.

public voidrestoreState(Bundle navState)

Restores all navigation controller state from a bundle.

public BundlesaveState()

Saves all navigation controller state to a Bundle.

public voidsetGraph(int graphResId)

Sets the navigation graph to the specified resource.

public voidsetGraph(int graphResId, Bundle startDestinationArgs)

Sets the navigation graph to the specified resource.

public voidsetGraph(NavGraph graph)

Sets the navigation graph to the specified graph.

public voidsetGraph(NavGraph graph, Bundle startDestinationArgs)

Sets the navigation graph to the specified graph.

public voidsetNavigatorProvider(NavigatorProvider navigatorProvider)

Sets the navigator provider to the specified provider.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final java.lang.String KEY_DEEP_LINK_INTENT

The that triggered a deep link to the current destination.

Constructors

public NavController(Context context)

Constructs a new controller for a given . Controllers should not be used outside of their context and retain a hard reference to the context supplied. If you need a global controller, pass .

Apps should generally not construct controllers, instead obtain a relevant controller directly from a navigation host via NavHost.getNavController() or by using one of the utility methods on the Navigation class.

Note that controllers that are not constructed with an context (or a wrapped activity context) will only be able to navigate to or when navigating to new activities.

Parameters:

context: context for this controller

Methods

public java.util.Deque<NavBackStackEntry> getBackStack()

Retrieve the current back stack.

Returns:

The current back stack.

public NavigatorProvider getNavigatorProvider()

Retrieve the NavController's NavigatorProvider. All Navigators used to construct the navigation graph for this nav controller should be added to this navigator provider before the graph is constructed.

Generally, the Navigators are set for you by the NavHost hosting this NavController and you do not need to manually interact with the navigator provider.

Returns:

The NavigatorProvider used by this NavController.

public void setNavigatorProvider(NavigatorProvider navigatorProvider)

Sets the navigator provider to the specified provider. This can only be called before the graph is set via setGraph().

Parameters:

navigatorProvider: NavigatorProvider to set

public void addOnDestinationChangedListener(NavController.OnDestinationChangedListener listener)

Adds an NavController.OnDestinationChangedListener to this controller to receive a callback whenever the NavController.getCurrentDestination() or its arguments change.

The current destination, if any, will be immediately sent to your listener.

Parameters:

listener: the listener to receive events

public void removeOnDestinationChangedListener(NavController.OnDestinationChangedListener listener)

Removes an NavController.OnDestinationChangedListener from this controller. It will no longer receive callbacks.

Parameters:

listener: the listener to remove

public boolean popBackStack()

Attempts to pop the controller's back stack. Analogous to when the user presses the system button when the associated navigation host has focus.

Returns:

true if the stack was popped and the user has been navigated to another destination, false otherwise

public boolean popBackStack(int destinationId, boolean inclusive)

Attempts to pop the controller's back stack back to a specific destination.

Parameters:

destinationId: The topmost destination to retain
inclusive: Whether the given destination should also be popped.

Returns:

true if the stack was popped at least once and the user has been navigated to another destination, false otherwise

public boolean navigateUp()

Attempts to navigate up in the navigation hierarchy. Suitable for when the user presses the "Up" button marked with a left (or start)-facing arrow in the upper left (or starting) corner of the app UI.

The intended behavior of Up differs from Back when the user did not reach the current destination from the application's own task. e.g. if the user is viewing a document or link in the current app in an activity hosted on another app's task where the user clicked the link. In this case the current activity (determined by the context used to create this NavController) will be and the user will be taken to an appropriate destination in this app on its own task.

Returns:

true if navigation was successful, false otherwise

public NavInflater getNavInflater()

Returns the inflater for this controller.

Returns:

inflater for loading navigation resources

public void setGraph(int graphResId)

Sets the navigation graph to the specified resource. Any current navigation graph data (including back stack) will be replaced.

The inflated graph can be retrieved via NavController.getGraph().

Parameters:

graphResId: resource id of the navigation graph to inflate

See also: NavController.getNavInflater(), NavController.setGraph(NavGraph), NavController.getGraph()

public void setGraph(int graphResId, Bundle startDestinationArgs)

Sets the navigation graph to the specified resource. Any current navigation graph data (including back stack) will be replaced.

The inflated graph can be retrieved via NavController.getGraph().

Parameters:

graphResId: resource id of the navigation graph to inflate
startDestinationArgs: arguments to send to the start destination of the graph

See also: NavController.getNavInflater(), NavController.setGraph(NavGraph, Bundle), NavController.getGraph()

public void setGraph(NavGraph graph)

Sets the navigation graph to the specified graph. Any current navigation graph data (including back stack) will be replaced.

The graph can be retrieved later via NavController.getGraph().

Parameters:

graph: graph to set

See also: NavController.setGraph(int), NavController.getGraph()

public void setGraph(NavGraph graph, Bundle startDestinationArgs)

Sets the navigation graph to the specified graph. Any current navigation graph data (including back stack) will be replaced.

The graph can be retrieved later via NavController.getGraph().

Parameters:

graph: graph to set

See also: NavController.setGraph(int, Bundle), NavController.getGraph()

public boolean handleDeepLink(Intent intent)

Checks the given Intent for a Navigation deep link and navigates to the deep link if present. This is called automatically for you the first time you set the graph if you've passed in an as the context when constructing this NavController, but should be manually called if your Activity receives new Intents in .

The types of Intents that are supported include:

The navigation graph should be set before calling this method.

Parameters:

intent: The Intent that may contain a valid deep link

Returns:

True if the navigation controller found a valid deep link and navigated to it.

See also: NavDestination.addDeepLink(NavDeepLink)

public NavGraph getGraph()

Gets the topmost navigation graph associated with this NavController.

See also: NavController.setGraph(int), NavController.setGraph(NavGraph)

public NavDestination getCurrentDestination()

Gets the current destination.

public void navigate(int resId)

Navigate to a destination from the current navigation graph. This supports both navigating via an action and directly navigating to a destination.

Parameters:

resId: an action id or a destination id to navigate to

public void navigate(int resId, Bundle args)

Navigate to a destination from the current navigation graph. This supports both navigating via an action and directly navigating to a destination.

Parameters:

resId: an action id or a destination id to navigate to
args: arguments to pass to the destination

public void navigate(int resId, Bundle args, NavOptions navOptions)

Navigate to a destination from the current navigation graph. This supports both navigating via an action and directly navigating to a destination.

Parameters:

resId: an action id or a destination id to navigate to
args: arguments to pass to the destination
navOptions: special options for this navigation operation

public void navigate(int resId, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras)

Navigate to a destination from the current navigation graph. This supports both navigating via an action and directly navigating to a destination.

Parameters:

resId: an action id or a destination id to navigate to
args: arguments to pass to the destination
navOptions: special options for this navigation operation
navigatorExtras: extras to pass to the Navigator

public void navigate(Uri deepLink)

Navigate to a destination via the given deep link . NavDestination.hasDeepLink(Uri) should be called on the navigation graph prior to calling this method to check if the deep link is valid. If an invalid deep link is given, an java.lang.IllegalArgumentException will be thrown.

Parameters:

deepLink: deepLink to the destination reachable from the current NavGraph

See also: NavController

public void navigate(Uri deepLink, NavOptions navOptions)

Navigate to a destination via the given deep link . NavDestination.hasDeepLink(Uri) should be called on the navigation graph prior to calling this method to check if the deep link is valid. If an invalid deep link is given, an java.lang.IllegalArgumentException will be thrown.

Parameters:

deepLink: deepLink to the destination reachable from the current NavGraph
navOptions: special options for this navigation operation

See also: NavController

public void navigate(Uri deepLink, NavOptions navOptions, Navigator.Extras navigatorExtras)

Navigate to a destination via the given deep link . NavDestination.hasDeepLink(Uri) should be called on the navigation graph prior to calling this method to check if the deep link is valid. If an invalid deep link is given, an java.lang.IllegalArgumentException will be thrown.

Parameters:

deepLink: deepLink to the destination reachable from the current NavGraph
navOptions: special options for this navigation operation
navigatorExtras: extras to pass to the Navigator

See also: NavController

public void navigate(NavDirections directions, Navigator.Extras navigatorExtras)

Navigate via the given NavDirections

Parameters:

directions: directions that describe this navigation operation
navigatorExtras: extras to pass to the Navigator

public NavDeepLinkBuilder createDeepLink()

Create a deep link to a destination within this NavController.

Returns:

a NavDeepLinkBuilder suitable for constructing a deep link

public Bundle saveState()

Saves all navigation controller state to a Bundle.

State may be restored from a bundle returned from this method by calling NavController.restoreState(Bundle). Saving controller state is the responsibility of a NavHost.

Returns:

saved state for this controller

public void restoreState(Bundle navState)

Restores all navigation controller state from a bundle. This should be called before any call to NavController.setGraph(int).

State may be saved to a bundle by calling NavController.saveState(). Restoring controller state is the responsibility of a NavHost.

Parameters:

navState: state bundle to restore

public ViewModelStoreOwner getViewModelStoreOwner(int navGraphId)

Gets the ViewModelStoreOwner for a NavGraph. This can be passed to ViewModelProvider to retrieve a ViewModel that is scoped to the navigation graph - it will be cleared when the navigation graph is popped off the back stack.

Parameters:

navGraphId: ID of a NavGraph that exists on the back stack

public NavBackStackEntry getBackStackEntry(int destinationId)

Gets the topmost NavBackStackEntry for a destination id.

This is always safe to use with the current destination or its parent or grandparent navigation graphs as these destinations are guaranteed to be on the back stack.

Parameters:

destinationId: ID of a destination that exists on the back stack

public NavBackStackEntry getCurrentBackStackEntry()

Gets the topmost NavBackStackEntry.

Returns:

the topmost entry on the back stack or null if the back stack is empty

public NavBackStackEntry getPreviousBackStackEntry()

Gets the previous visible NavBackStackEntry.

This skips over any NavBackStackEntry that is associated with a NavGraph.

Returns:

the previous visible entry on the back stack or null if the back stack has less than two visible entries

Source

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.navigation;

import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;

import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.annotation.CallSuper;
import androidx.annotation.IdRes;
import androidx.annotation.NavigationRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.app.TaskStackBuilder;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelStore;
import androidx.lifecycle.ViewModelStoreOwner;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * NavController manages app navigation within a {@link NavHost}.
 *
 * <p>Apps will generally obtain a controller directly from a host, or by using one of the utility
 * methods on the {@link Navigation} class rather than create a controller directly.</p>
 *
 * <p>Navigation flows and destinations are determined by the
 * {@link NavGraph navigation graph} owned by the controller. These graphs are typically
 * {@link #getNavInflater() inflated} from an Android resource, but, like views, they can also
 * be constructed or combined programmatically or for the case of dynamic navigation structure.
 * (For example, if the navigation structure of the application is determined by live data obtained'
 * from a remote server.)</p>
 */
public class NavController {
    private static final String TAG = "NavController";
    private static final String KEY_NAVIGATOR_STATE =
            "android-support-nav:controller:navigatorState";
    private static final String KEY_NAVIGATOR_STATE_NAMES =
            "android-support-nav:controller:navigatorState:names";
    private static final String KEY_BACK_STACK =
            "android-support-nav:controller:backStack";
    static final String KEY_DEEP_LINK_IDS = "android-support-nav:controller:deepLinkIds";
    static final String KEY_DEEP_LINK_EXTRAS =
            "android-support-nav:controller:deepLinkExtras";
    static final String KEY_DEEP_LINK_HANDLED =
            "android-support-nav:controller:deepLinkHandled";
    /**
     * The {@link Intent} that triggered a deep link to the current destination.
     */
    public static final @NonNull String KEY_DEEP_LINK_INTENT =
            "android-support-nav:controller:deepLinkIntent";

    private final Context mContext;
    private Activity mActivity;
    private NavInflater mInflater;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    NavGraph mGraph;
    private Bundle mNavigatorStateToRestore;
    private Parcelable[] mBackStackToRestore;
    private boolean mDeepLinkHandled;

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Deque<NavBackStackEntry> mBackStack = new ArrayDeque<>();

    private LifecycleOwner mLifecycleOwner;
    private NavControllerViewModel mViewModel;

    private NavigatorProvider mNavigatorProvider = new NavigatorProvider();

    private final CopyOnWriteArrayList<OnDestinationChangedListener>
            mOnDestinationChangedListeners = new CopyOnWriteArrayList<>();

    private final LifecycleObserver mLifecycleObserver = new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (mGraph != null) {
                for (NavBackStackEntry entry : mBackStack) {
                    entry.handleLifecycleEvent(event);
                }
            }
        }
    };
    private final OnBackPressedCallback mOnBackPressedCallback =
            new OnBackPressedCallback(false) {
        @Override
        public void handleOnBackPressed() {
            popBackStack();
        }
    };
    private boolean mEnableOnBackPressedCallback = true;

    /**
     * OnDestinationChangedListener receives a callback when the
     * {@link #getCurrentDestination()} or its arguments change.
     */
    public interface OnDestinationChangedListener {
        /**
         * Callback for when the {@link #getCurrentDestination()} or its arguments change.
         * This navigation may be to a destination that has not been seen before, or one that
         * was previously on the back stack. This method is called after navigation is complete,
         * but associated transitions may still be playing.
         *
         * @param controller the controller that navigated
         * @param destination the new destination
         * @param arguments the arguments passed to the destination
         */
        void onDestinationChanged(@NonNull NavController controller,
                @NonNull NavDestination destination, @Nullable Bundle arguments);
    }

    /**
     * Constructs a new controller for a given {@link Context}. Controllers should not be
     * used outside of their context and retain a hard reference to the context supplied.
     * If you need a global controller, pass {@link Context#getApplicationContext()}.
     *
     * <p>Apps should generally not construct controllers, instead obtain a relevant controller
     * directly from a navigation host via {@link NavHost#getNavController()} or by using one of
     * the utility methods on the {@link Navigation} class.</p>
     *
     * <p>Note that controllers that are not constructed with an {@link Activity} context
     * (or a wrapped activity context) will only be able to navigate to
     * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK new tasks} or
     * {@link android.content.Intent#FLAG_ACTIVITY_NEW_DOCUMENT new document tasks} when
     * navigating to new activities.</p>
     *
     * @param context context for this controller
     */
    public NavController(@NonNull Context context) {
        mContext = context;
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                mActivity = (Activity) context;
                break;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
        mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
        mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
    }

    /**
     * Retrieve the current back stack.
     *
     * @return The current back stack.
     * @hide
     */
    @NonNull
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public Deque<NavBackStackEntry> getBackStack() {
        return mBackStack;
    }

    @NonNull
    Context getContext() {
        return mContext;
    }

    /**
     * Retrieve the NavController's {@link NavigatorProvider}. All {@link Navigator Navigators} used
     * to construct the {@link NavGraph navigation graph} for this nav controller should be added
     * to this navigator provider before the graph is constructed.
     * <p>
     * Generally, the Navigators are set for you by the {@link NavHost} hosting this NavController
     * and you do not need to manually interact with the navigator provider.
     * </p>
     * @return The {@link NavigatorProvider} used by this NavController.
     */
    @NonNull
    public NavigatorProvider getNavigatorProvider() {
        return mNavigatorProvider;
    }

    /**
     * Sets the {@link NavigatorProvider navigator provider} to the specified provider. This can
     * only be called before the graph is set via {@code setGraph()}.
     *
     * @param navigatorProvider {@link NavigatorProvider} to set
     * @throws IllegalStateException If this is called after {@code setGraph()}
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public void setNavigatorProvider(@NonNull NavigatorProvider navigatorProvider) {
        if (!mBackStack.isEmpty()) {
            throw new IllegalStateException("NavigatorProvider must be set before setGraph call");
        }
        mNavigatorProvider = navigatorProvider;
    }

    /**
     * Adds an {@link OnDestinationChangedListener} to this controller to receive a callback
     * whenever the {@link #getCurrentDestination()} or its arguments change.
     *
     * <p>The current destination, if any, will be immediately sent to your listener.</p>
     *
     * @param listener the listener to receive events
     */
    public void addOnDestinationChangedListener(@NonNull OnDestinationChangedListener listener) {
        // Inform the new listener of our current state, if any
        if (!mBackStack.isEmpty()) {
            NavBackStackEntry backStackEntry = mBackStack.peekLast();
            listener.onDestinationChanged(this, backStackEntry.getDestination(),
                    backStackEntry.getArguments());
        }
        mOnDestinationChangedListeners.add(listener);
    }

    /**
     * Removes an {@link OnDestinationChangedListener} from this controller.
     * It will no longer receive callbacks.
     *
     * @param listener the listener to remove
     */
    public void removeOnDestinationChangedListener(
            @NonNull OnDestinationChangedListener listener) {
        mOnDestinationChangedListeners.remove(listener);
    }

    /**
     * Attempts to pop the controller's back stack. Analogous to when the user presses
     * the system {@link android.view.KeyEvent#KEYCODE_BACK Back} button when the associated
     * navigation host has focus.
     *
     * @return true if the stack was popped and the user has been navigated to another
     * destination, false otherwise
     */
    public boolean popBackStack() {
        if (mBackStack.isEmpty()) {
            // Nothing to pop if the back stack is empty
            return false;
        }
        // Pop just the current destination off the stack
        return popBackStack(getCurrentDestination().getId(), true);
    }

    /**
     * Attempts to pop the controller's back stack back to a specific destination.
     *
     * @param destinationId The topmost destination to retain
     * @param inclusive Whether the given destination should also be popped.
     *
     * @return true if the stack was popped at least once and the user has been navigated to
     * another destination, false otherwise
     */
    public boolean popBackStack(@IdRes int destinationId, boolean inclusive) {
        boolean popped = popBackStackInternal(destinationId, inclusive);
        // Only return true if the pop succeeded and we've dispatched
        // the change to a new destination
        return popped && dispatchOnDestinationChanged();
    }

    /**
     * Attempts to pop the controller's back stack back to a specific destination. This does
     * <strong>not</strong> handle calling {@link #dispatchOnDestinationChanged()}
     *
     * @param destinationId The topmost destination to retain
     * @param inclusive Whether the given destination should also be popped.
     *
     * @return true if the stack was popped at least once, false otherwise
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
        if (mBackStack.isEmpty()) {
            // Nothing to pop if the back stack is empty
            return false;
        }
        ArrayList<Navigator<?>> popOperations = new ArrayList<>();
        Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
        boolean foundDestination = false;
        while (iterator.hasNext()) {
            NavDestination destination = iterator.next().getDestination();
            Navigator<?> navigator = mNavigatorProvider.getNavigator(
                    destination.getNavigatorName());
            if (inclusive || destination.getId() != destinationId) {
                popOperations.add(navigator);
            }
            if (destination.getId() == destinationId) {
                foundDestination = true;
                break;
            }
        }
        if (!foundDestination) {
            // We were passed a destinationId that doesn't exist on our back stack.
            // Better to ignore the popBackStack than accidentally popping the entire stack
            String destinationName = NavDestination.getDisplayName(mContext, destinationId);
            Log.i(TAG, "Ignoring popBackStack to destination " + destinationName
                    + " as it was not found on the current back stack");
            return false;
        }
        boolean popped = false;
        for (Navigator<?> navigator : popOperations) {
            if (navigator.popBackStack()) {
                NavBackStackEntry entry = mBackStack.removeLast();
                entry.setMaxLifecycle(Lifecycle.State.DESTROYED);
                if (mViewModel != null) {
                    mViewModel.clear(entry.mId);
                }
                popped = true;
            } else {
                // The pop did not complete successfully, so stop immediately
                break;
            }
        }
        updateOnBackPressedCallbackEnabled();
        return popped;
    }

    /**
     * Attempts to navigate up in the navigation hierarchy. Suitable for when the
     * user presses the "Up" button marked with a left (or start)-facing arrow in the upper left
     * (or starting) corner of the app UI.
     *
     * <p>The intended behavior of Up differs from {@link #popBackStack() Back} when the user
     * did not reach the current destination from the application's own task. e.g. if the user
     * is viewing a document or link in the current app in an activity hosted on another app's
     * task where the user clicked the link. In this case the current activity (determined by the
     * context used to create this NavController) will be {@link Activity#finish() finished} and
     * the user will be taken to an appropriate destination in this app on its own task.</p>
     *
     * @return true if navigation was successful, false otherwise
     */
    public boolean navigateUp() {
        if (getDestinationCountOnBackStack() == 1) {
            // If there's only one entry, then we've deep linked into a specific destination
            // on another task so we need to find the parent and start our task from there
            NavDestination currentDestination = getCurrentDestination();
            int destId = currentDestination.getId();
            NavGraph parent = currentDestination.getParent();
            while (parent != null) {
                if (parent.getStartDestination() != destId) {
                    final Bundle args = new Bundle();

                    if (mActivity != null && mActivity.getIntent() != null) {
                        final Uri data = mActivity.getIntent().getData();

                        // We were started via a URI intent.
                        if (data != null) {
                            // Include the original deep link Intent so the Destinations can
                            // synthetically generate additional arguments as necessary.
                            args.putParcelable(KEY_DEEP_LINK_INTENT, mActivity.getIntent());
                            NavDestination.DeepLinkMatch matchingDeepLink = mGraph.matchDeepLink(
                                    new NavDeepLinkRequest(mActivity.getIntent()));
                            if (matchingDeepLink != null) {
                                Bundle destinationArgs =
                                        matchingDeepLink.getDestination().addInDefaultArgs(
                                                matchingDeepLink.getMatchingArgs()
                                        );
                                args.putAll(destinationArgs);
                            }
                        }
                    }

                    TaskStackBuilder parentIntents = new NavDeepLinkBuilder(this)
                            .setDestination(parent.getId())
                            .setArguments(args)
                            .createTaskStackBuilder();

                    parentIntents.startActivities();
                    if (mActivity != null) {
                        mActivity.finish();
                    }
                    return true;
                }
                destId = parent.getId();
                parent = parent.getParent();
            }
            // We're already at the startDestination of the graph so there's no 'Up' to go to
            return false;
        } else {
            return popBackStack();
        }
    }

    /**
     * Gets the number of non-NavGraph destinations on the back stack
     */
    private int getDestinationCountOnBackStack() {
        int count = 0;
        for (NavBackStackEntry entry : mBackStack) {
            if (!(entry.getDestination() instanceof NavGraph)) {
                count++;
            }
        }
        return count;
    }

    /**
     * Dispatch changes to all OnDestinationChangedListeners.
     * <p>
     * If the back stack is empty, no events get dispatched.
     *
     * @return If changes were dispatched.
     */
    private boolean dispatchOnDestinationChanged() {
        // We never want to leave NavGraphs on the top of the stack
        //noinspection StatementWithEmptyBody
        while (!mBackStack.isEmpty()
                && mBackStack.peekLast().getDestination() instanceof NavGraph
                && popBackStackInternal(mBackStack.peekLast().getDestination().getId(), true)) {
            // Keep popping
        }
        if (!mBackStack.isEmpty()) {
            // First determine what the current resumed destination is and, if and only if
            // the current resumed destination is a FloatingWindow, what destination is
            // underneath it that must remain started.
            NavDestination nextResumed = mBackStack.peekLast().getDestination();
            NavDestination nextStarted = null;
            if (nextResumed instanceof FloatingWindow) {
                // Find the next destination in the back stack as that destination
                // should still be STARTED when the FloatingWindow destination is above it.
                Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
                while (iterator.hasNext()) {
                    NavDestination destination = iterator.next().getDestination();
                    if (!(destination instanceof NavGraph)
                            && !(destination instanceof FloatingWindow)) {
                        nextStarted = destination;
                        break;
                    }
                }
            }
            // First iterate downward through the stack, applying downward Lifecycle
            // transitions and capturing any upward Lifecycle transitions to apply afterwards.
            // This ensures proper nesting where parent navigation graphs are started before
            // their children and stopped only after their children are stopped.
            HashMap<NavBackStackEntry, Lifecycle.State> upwardStateTransitions = new HashMap<>();
            Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
            while (iterator.hasNext()) {
                NavBackStackEntry entry = iterator.next();
                Lifecycle.State currentMaxLifecycle = entry.getMaxLifecycle();
                NavDestination destination = entry.getDestination();
                if (nextResumed != null && destination.getId() == nextResumed.getId()) {
                    // Upward Lifecycle transitions need to be done afterwards so that
                    // the parent navigation graph is resumed before their children
                    if (currentMaxLifecycle != Lifecycle.State.RESUMED) {
                        upwardStateTransitions.put(entry, Lifecycle.State.RESUMED);
                    }
                    nextResumed = nextResumed.getParent();
                } else if (nextStarted != null && destination.getId() == nextStarted.getId()) {
                    if (currentMaxLifecycle == Lifecycle.State.RESUMED) {
                        // Downward transitions should be done immediately so children are
                        // paused before their parent navigation graphs
                        entry.setMaxLifecycle(Lifecycle.State.STARTED);
                    } else if (currentMaxLifecycle != Lifecycle.State.STARTED) {
                        // Upward Lifecycle transitions need to be done afterwards so that
                        // the parent navigation graph is started before their children
                        upwardStateTransitions.put(entry, Lifecycle.State.STARTED);
                    }
                    nextStarted = nextStarted.getParent();
                } else {
                    entry.setMaxLifecycle(Lifecycle.State.CREATED);
                }
            }
            // Apply all upward Lifecycle transitions by iterating through the stack again,
            // this time applying the new lifecycle to the parent navigation graphs first
            iterator = mBackStack.iterator();
            while (iterator.hasNext()) {
                NavBackStackEntry entry = iterator.next();
                Lifecycle.State newState = upwardStateTransitions.get(entry);
                if (newState != null) {
                    entry.setMaxLifecycle(newState);
                } else {
                    // Ensure the state is up to date
                    entry.updateState();
                }
            }

            // Now call all registered OnDestinationChangedListener instances
            NavBackStackEntry backStackEntry = mBackStack.peekLast();
            for (OnDestinationChangedListener listener :
                    mOnDestinationChangedListeners) {
                listener.onDestinationChanged(this, backStackEntry.getDestination(),
                        backStackEntry.getArguments());
            }
            return true;
        }
        return false;
    }

    /**
     * Returns the {@link NavInflater inflater} for this controller.
     *
     * @return inflater for loading navigation resources
     */
    @NonNull
    public NavInflater getNavInflater() {
        if (mInflater == null) {
            mInflater = new NavInflater(mContext, mNavigatorProvider);
        }
        return mInflater;
    }

    /**
     * Sets the {@link NavGraph navigation graph} to the specified resource.
     * Any current navigation graph data (including back stack) will be replaced.
     *
     * <p>The inflated graph can be retrieved via {@link #getGraph()}.</p>
     *
     * @param graphResId resource id of the navigation graph to inflate
     *
     * @see #getNavInflater()
     * @see #setGraph(NavGraph)
     * @see #getGraph
     */
    @CallSuper
    public void setGraph(@NavigationRes int graphResId) {
        setGraph(graphResId, null);
    }

    /**
     * Sets the {@link NavGraph navigation graph} to the specified resource.
     * Any current navigation graph data (including back stack) will be replaced.
     *
     * <p>The inflated graph can be retrieved via {@link #getGraph()}.</p>
     *
     * @param graphResId resource id of the navigation graph to inflate
     * @param startDestinationArgs arguments to send to the start destination of the graph
     *
     * @see #getNavInflater()
     * @see #setGraph(NavGraph, Bundle)
     * @see #getGraph
     */
    @CallSuper
    public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
        setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
    }

    /**
     * Sets the {@link NavGraph navigation graph} to the specified graph.
     * Any current navigation graph data (including back stack) will be replaced.
     *
     * <p>The graph can be retrieved later via {@link #getGraph()}.</p>
     *
     * @param graph graph to set
     * @see #setGraph(int)
     * @see #getGraph
     */
    @CallSuper
    public void setGraph(@NonNull NavGraph graph) {
        setGraph(graph, null);
    }

    /**
     * Sets the {@link NavGraph navigation graph} to the specified graph.
     * Any current navigation graph data (including back stack) will be replaced.
     *
     * <p>The graph can be retrieved later via {@link #getGraph()}.</p>
     *
     * @param graph graph to set
     * @see #setGraph(int, Bundle)
     * @see #getGraph
     */
    @CallSuper
    public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
        if (mGraph != null) {
            // Pop everything from the old graph off the back stack
            popBackStackInternal(mGraph.getId(), true);
        }
        mGraph = graph;
        onGraphCreated(startDestinationArgs);
    }

    private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
        if (mNavigatorStateToRestore != null) {
            ArrayList<String> navigatorNames = mNavigatorStateToRestore.getStringArrayList(
                    KEY_NAVIGATOR_STATE_NAMES);
            if (navigatorNames != null) {
                for (String name : navigatorNames) {
                    Navigator<?> navigator = mNavigatorProvider.getNavigator(name);
                    Bundle bundle = mNavigatorStateToRestore.getBundle(name);
                    if (bundle != null) {
                        navigator.onRestoreState(bundle);
                    }
                }
            }
        }
        if (mBackStackToRestore != null) {
            for (Parcelable parcelable : mBackStackToRestore) {
                NavBackStackEntryState state = (NavBackStackEntryState) parcelable;
                NavDestination node = findDestination(state.getDestinationId());
                if (node == null) {
                    final String dest = NavDestination.getDisplayName(mContext,
                            state.getDestinationId());
                    throw new IllegalStateException("Restoring the Navigation back stack failed:"
                            + " destination " + dest
                            + " cannot be found from the current destination "
                            + getCurrentDestination());
                }
                Bundle args = state.getArgs();
                if (args != null) {
                    args.setClassLoader(mContext.getClassLoader());
                }
                NavBackStackEntry entry = new NavBackStackEntry(mContext, node, args,
                        mLifecycleOwner, mViewModel,
                        state.getUUID(), state.getSavedState());
                mBackStack.add(entry);
            }
            updateOnBackPressedCallbackEnabled();
            mBackStackToRestore = null;
        }
        if (mGraph != null && mBackStack.isEmpty()) {
            boolean deepLinked = !mDeepLinkHandled && mActivity != null
                    && handleDeepLink(mActivity.getIntent());
            if (!deepLinked) {
                // Navigate to the first destination in the graph
                // if we haven't deep linked to a destination
                navigate(mGraph, startDestinationArgs, null, null);
            }
        } else {
            dispatchOnDestinationChanged();
        }
    }

    /**
     * Checks the given Intent for a Navigation deep link and navigates to the deep link if present.
     * This is called automatically for you the first time you set the graph if you've passed in an
     * {@link Activity} as the context when constructing this NavController, but should be manually
     * called if your Activity receives new Intents in {@link Activity#onNewIntent(Intent)}.
     * <p>
     * The types of Intents that are supported include:
     * <ul>
     *     <ol>Intents created by {@link NavDeepLinkBuilder} or
     *     {@link #createDeepLink()}. This assumes that the current graph shares
     *     the same hierarchy to get to the deep linked destination as when the deep link was
     *     constructed.</ol>
     *     <ol>Intents that include a {@link Intent#getData() data Uri}. This Uri will be checked
     *     against the Uri patterns in the {@link NavDeepLink NavDeepLinks} added via
     *     {@link NavDestination#addDeepLink(NavDeepLink)}.</ol>
     * </ul>
     * <p>The {@link #getGraph() navigation graph} should be set before calling this method.</p>
     * @param intent The Intent that may contain a valid deep link
     * @return True if the navigation controller found a valid deep link and navigated to it.
     * @see NavDestination#addDeepLink(NavDeepLink)
     */
    public boolean handleDeepLink(@Nullable Intent intent) {
        if (intent == null) {
            return false;
        }
        Bundle extras = intent.getExtras();
        int[] deepLink = extras != null ? extras.getIntArray(KEY_DEEP_LINK_IDS) : null;
        Bundle bundle = new Bundle();
        Bundle deepLinkExtras = extras != null ? extras.getBundle(KEY_DEEP_LINK_EXTRAS) : null;
        if (deepLinkExtras != null) {
            bundle.putAll(deepLinkExtras);
        }
        if ((deepLink == null || deepLink.length == 0) && intent.getData() != null) {
            NavDestination.DeepLinkMatch matchingDeepLink =
                    mGraph.matchDeepLink(new NavDeepLinkRequest(intent));
            if (matchingDeepLink != null) {
                NavDestination destination = matchingDeepLink.getDestination();
                deepLink = destination.buildDeepLinkIds();
                Bundle destinationArgs =
                        destination.addInDefaultArgs(matchingDeepLink.getMatchingArgs());
                bundle.putAll(destinationArgs);
            }
        }
        if (deepLink == null || deepLink.length == 0) {
            return false;
        }
        String invalidDestinationDisplayName =
                findInvalidDestinationDisplayNameInDeepLink(deepLink);
        if (invalidDestinationDisplayName != null) {
            Log.i(TAG, "Could not find destination " + invalidDestinationDisplayName
                    + " in the navigation graph, ignoring the deep link from " + intent);
            return false;
        }
        bundle.putParcelable(KEY_DEEP_LINK_INTENT, intent);
        int flags = intent.getFlags();
        if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0
                && (flags & Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0) {
            // Someone called us with NEW_TASK, but we don't know what state our whole
            // task stack is in, so we need to manually restart the whole stack to
            // ensure we're in a predictably good state.
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
            TaskStackBuilder taskStackBuilder = TaskStackBuilder
                    .create(mContext)
                    .addNextIntentWithParentStack(intent);
            taskStackBuilder.startActivities();
            if (mActivity != null) {
                mActivity.finish();
                // Disable second animation in case where the Activity is created twice.
                mActivity.overridePendingTransition(0, 0);
            }
            return true;
        }
        if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
            // Start with a cleared task starting at our root when we're on our own task
            if (!mBackStack.isEmpty()) {
                popBackStackInternal(mGraph.getId(), true);
            }
            int index = 0;
            while (index < deepLink.length) {
                int destinationId = deepLink[index++];
                NavDestination node = findDestination(destinationId);
                if (node == null) {
                    final String dest = NavDestination.getDisplayName(mContext, destinationId);
                    throw new IllegalStateException("Deep Linking failed:"
                            + " destination " + dest
                            + " cannot be found from the current destination "
                            + getCurrentDestination());
                }
                navigate(node, bundle,
                        new NavOptions.Builder().setEnterAnim(0).setExitAnim(0).build(), null);
            }
            return true;
        }
        // Assume we're on another apps' task and only start the final destination
        NavGraph graph = mGraph;
        for (int i = 0; i < deepLink.length; i++) {
            int destinationId = deepLink[i];
            NavDestination node = i == 0 ? mGraph : graph.findNode(destinationId);
            if (node == null) {
                final String dest = NavDestination.getDisplayName(mContext, destinationId);
                throw new IllegalStateException("Deep Linking failed:"
                        + " destination " + dest
                        + " cannot be found in graph " + graph);
            }
            if (i != deepLink.length - 1) {
                // We're not at the final NavDestination yet, so keep going through the chain
                graph = (NavGraph) node;
                // Automatically go down the navigation graph when
                // the start destination is also a NavGraph
                while (graph.findNode(graph.getStartDestination()) instanceof NavGraph) {
                    graph = (NavGraph) graph.findNode(graph.getStartDestination());
                }
            } else {
                // Navigate to the last NavDestination, clearing any existing destinations
                navigate(node, node.addInDefaultArgs(bundle), new NavOptions.Builder()
                        .setPopUpTo(mGraph.getId(), true)
                        .setEnterAnim(0).setExitAnim(0).build(), null);
            }
        }
        mDeepLinkHandled = true;
        return true;
    }

    /**
     * Looks through the deep link for invalid destinations, returning the display name of
     * any invalid destinations in the deep link array.
     *
     * @param deepLink array of deep link IDs that are expected to match the graph
     * @return The display name of the first destination not found in the graph or null if
     * all destinations were found in the graph.
     */
    @Nullable
    private String findInvalidDestinationDisplayNameInDeepLink(@NonNull int[] deepLink) {
        NavGraph graph = mGraph;
        for (int i = 0; i < deepLink.length; i++) {
            int destinationId = deepLink[i];
            NavDestination node = i == 0
                    ? (mGraph.getId() == destinationId ? mGraph : null)
                    : graph.findNode(destinationId);
            if (node == null) {
                return NavDestination.getDisplayName(mContext, destinationId);
            }
            if (i != deepLink.length - 1) {
                // We're not at the final NavDestination yet, so keep going through the chain
                graph = (NavGraph) node;
                // Automatically go down the navigation graph when
                // the start destination is also a NavGraph
                while (graph.findNode(graph.getStartDestination()) instanceof NavGraph) {
                    graph = (NavGraph) graph.findNode(graph.getStartDestination());
                }
            }
        }
        // We found every destination in the deepLink array, yay!
        return null;
    }

    /**
     * Gets the topmost navigation graph associated with this NavController.
     *
     * @see #setGraph(int)
     * @see #setGraph(NavGraph)
     * @throws IllegalStateException if called before <code>setGraph()</code>.
     */
    @NonNull
    public NavGraph getGraph() {
        if (mGraph == null) {
            throw new IllegalStateException("You must call setGraph() before calling getGraph()");
        }
        return mGraph;
    }

    /**
     * Gets the current destination.
     */
    @Nullable
    public NavDestination getCurrentDestination() {
        NavBackStackEntry entry = getCurrentBackStackEntry();
        return entry != null ? entry.getDestination() : null;
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    NavDestination findDestination(@IdRes int destinationId) {
        if (mGraph == null) {
            return null;
        }
        if (mGraph.getId() == destinationId) {
            return mGraph;
        }
        NavDestination currentNode = mBackStack.isEmpty()
                ? mGraph
                : mBackStack.getLast().getDestination();
        NavGraph currentGraph = currentNode instanceof NavGraph
                ? (NavGraph) currentNode
                : currentNode.getParent();
        return currentGraph.findNode(destinationId);
    }

    /**
     * Navigate to a destination from the current navigation graph. This supports both navigating
     * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
     *
     * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
     *              navigate to
     */
    public void navigate(@IdRes int resId) {
        navigate(resId, null);
    }

    /**
     * Navigate to a destination from the current navigation graph. This supports both navigating
     * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
     *
     * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
     *              navigate to
     * @param args arguments to pass to the destination
     */
    public void navigate(@IdRes int resId, @Nullable Bundle args) {
        navigate(resId, args, null);
    }

    /**
     * Navigate to a destination from the current navigation graph. This supports both navigating
     * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
     *
     * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
     *              navigate to
     * @param args arguments to pass to the destination
     * @param navOptions special options for this navigation operation
     */
    public void navigate(@IdRes int resId, @Nullable Bundle args,
            @Nullable NavOptions navOptions) {
        navigate(resId, args, navOptions, null);
    }

    /**
     * Navigate to a destination from the current navigation graph. This supports both navigating
     * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
     *
     * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
     *              navigate to
     * @param args arguments to pass to the destination
     * @param navOptions special options for this navigation operation
     * @param navigatorExtras extras to pass to the Navigator
     */
    @SuppressWarnings("deprecation")
    public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
            @Nullable Navigator.Extras navigatorExtras) {
        NavDestination currentNode = mBackStack.isEmpty()
                ? mGraph
                : mBackStack.getLast().getDestination();
        if (currentNode == null) {
            throw new IllegalStateException("no current navigation node");
        }
        @IdRes int destId = resId;
        final NavAction navAction = currentNode.getAction(resId);
        Bundle combinedArgs = null;
        if (navAction != null) {
            if (navOptions == null) {
                navOptions = navAction.getNavOptions();
            }
            destId = navAction.getDestinationId();
            Bundle navActionArgs = navAction.getDefaultArguments();
            if (navActionArgs != null) {
                combinedArgs = new Bundle();
                combinedArgs.putAll(navActionArgs);
            }
        }

        if (args != null) {
            if (combinedArgs == null) {
                combinedArgs = new Bundle();
            }
            combinedArgs.putAll(args);
        }

        if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
            popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
            return;
        }

        if (destId == 0) {
            throw new IllegalArgumentException("Destination id == 0 can only be used"
                    + " in conjunction with a valid navOptions.popUpTo");
        }

        NavDestination node = findDestination(destId);
        if (node == null) {
            final String dest = NavDestination.getDisplayName(mContext, destId);
            if (navAction != null) {
                throw new IllegalArgumentException("Navigation destination " + dest
                        + " referenced from action "
                        + NavDestination.getDisplayName(mContext, resId)
                        + " cannot be found from the current destination " + currentNode);
            } else {
                throw new IllegalArgumentException("Navigation action/destination " + dest
                        + " cannot be found from the current destination " + currentNode);
            }
        }
        navigate(node, combinedArgs, navOptions, navigatorExtras);
    }

    /**
     * Navigate to a destination via the given deep link {@link Uri}.
     * {@link NavDestination#hasDeepLink(Uri)} should be called on
     * {@link #getGraph() the navigation graph} prior to calling this method to check if the deep
     * link is valid. If an invalid deep link is given, an {@link IllegalArgumentException} will be
     * thrown.
     *
     * @param deepLink deepLink to the destination reachable from the current NavGraph
     * @see #navigate(NavDeepLinkRequest)
     */
    public void navigate(@NonNull Uri deepLink) {
        navigate(new NavDeepLinkRequest(deepLink, null, null));
    }

    /**
     * Navigate to a destination via the given deep link {@link Uri}.
     * {@link NavDestination#hasDeepLink(Uri)} should be called on
     * {@link #getGraph() the navigation graph} prior to calling this method to check if the deep
     * link is valid. If an invalid deep link is given, an {@link IllegalArgumentException} will be
     * thrown.
     *
     * @param deepLink deepLink to the destination reachable from the current NavGraph
     * @param navOptions special options for this navigation operation
     * @see #navigate(NavDeepLinkRequest, NavOptions)
     */
    public void navigate(@NonNull Uri deepLink, @Nullable NavOptions navOptions) {
        navigate(new NavDeepLinkRequest(deepLink, null, null), navOptions);
    }

    /**
     * Navigate to a destination via the given deep link {@link Uri}.
     * {@link NavDestination#hasDeepLink(Uri)} should be called on
     * {@link #getGraph() the navigation graph} prior to calling this method to check if the deep
     * link is valid. If an invalid deep link is given, an {@link IllegalArgumentException} will be
     * thrown.
     *
     * @param deepLink deepLink to the destination reachable from the current NavGraph
     * @param navOptions special options for this navigation operation
     * @param navigatorExtras extras to pass to the Navigator
     * @see #navigate(NavDeepLinkRequest, NavOptions, Navigator.Extras)
     */
    public void navigate(@NonNull Uri deepLink, @Nullable NavOptions navOptions,
            @Nullable Navigator.Extras navigatorExtras) {
        navigate(new NavDeepLinkRequest(deepLink, null, null), navOptions, navigatorExtras);
    }

    /**
     * Navigate to a destination via the given {@link NavDeepLinkRequest}.
     * {@link NavDestination#hasDeepLink(NavDeepLinkRequest)} should be called on
     * {@link #getGraph() the navigation graph} prior to calling this method to check if the deep
     * link is valid. If an invalid deep link is given, an {@link IllegalArgumentException} will be
     * thrown.
     *
     * @param request deepLinkRequest to the destination reachable from the current NavGraph
     */
    public void navigate(@NonNull NavDeepLinkRequest request) {
        navigate(request, null);
    }

    /**
     * Navigate to a destination via the given {@link NavDeepLinkRequest}.
     * {@link NavDestination#hasDeepLink(NavDeepLinkRequest)} should be called on
     * {@link #getGraph() the navigation graph} prior to calling this method to check if the deep
     * link is valid. If an invalid deep link is given, an {@link IllegalArgumentException} will be
     * thrown.
     *
     * @param request deepLinkRequest to the destination reachable from the current NavGraph
     * @param navOptions special options for this navigation operation
     */
    public void navigate(@NonNull NavDeepLinkRequest request, @Nullable NavOptions navOptions) {
        navigate(request, navOptions, null);
    }

    /**
     * Navigate to a destination via the given {@link NavDeepLinkRequest}.
     * {@link NavDestination#hasDeepLink(NavDeepLinkRequest)} should be called on
     * {@link #getGraph() the navigation graph} prior to calling this method to check if the deep
     * link is valid. If an invalid deep link is given, an {@link IllegalArgumentException} will be
     * thrown.
     *
     * @param request deepLinkRequest to the destination reachable from the current NavGraph
     * @param navOptions special options for this navigation operation
     * @param navigatorExtras extras to pass to the Navigator
     */
    public void navigate(@NonNull NavDeepLinkRequest request, @Nullable NavOptions navOptions,
            @Nullable Navigator.Extras navigatorExtras) {
        NavDestination.DeepLinkMatch deepLinkMatch =
                mGraph.matchDeepLink(request);
        if (deepLinkMatch != null) {
            NavDestination destination = deepLinkMatch.getDestination();
            Bundle args = destination.addInDefaultArgs(deepLinkMatch.getMatchingArgs());
            NavDestination node = deepLinkMatch.getDestination();
            navigate(node, args, navOptions, navigatorExtras);
        } else {
            throw new IllegalArgumentException("Navigation destination that matches request "
                    + request + " cannot be found in the navigation graph " + mGraph);
        }
    }

    private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        boolean launchSingleTop = false;
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        if (newDest != null) {
            if (!(newDest instanceof FloatingWindow)) {
                // We've successfully navigating to the new destination, which means
                // we should pop any FloatingWindow destination off the back stack
                // before updating the back stack with our new destination
                //noinspection StatementWithEmptyBody
                while (!mBackStack.isEmpty()
                        && mBackStack.peekLast().getDestination() instanceof FloatingWindow
                        && popBackStackInternal(
                                mBackStack.peekLast().getDestination().getId(), true)) {
                    // Keep popping
                }
            }

            // When you navigate() to a NavGraph, we need to ensure that a new instance
            // is always created vs reusing an existing copy of that destination
            ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
            NavDestination destination = newDest;
            if (node instanceof NavGraph) {
                do {
                    NavGraph parent = destination.getParent();
                    if (parent != null) {
                        NavBackStackEntry entry = new NavBackStackEntry(mContext, parent,
                                finalArgs, mLifecycleOwner, mViewModel);
                        hierarchy.addFirst(entry);
                        // Pop any orphaned copy of that navigation graph off the back stack
                        if (!mBackStack.isEmpty()
                                && mBackStack.getLast().getDestination() == parent) {
                            popBackStackInternal(parent.getId(), true);
                        }
                    }
                    destination = parent;
                } while (destination != null && destination != node);
            }

            // Now collect the set of all intermediate NavGraphs that need to be put onto
            // the back stack
            destination = hierarchy.isEmpty()
                    ? newDest
                    : hierarchy.getFirst().getDestination();
            while (destination != null && findDestination(destination.getId()) == null) {
                NavGraph parent = destination.getParent();
                if (parent != null) {
                    NavBackStackEntry entry = new NavBackStackEntry(mContext, parent, finalArgs,
                            mLifecycleOwner, mViewModel);
                    hierarchy.addFirst(entry);
                }
                destination = parent;
            }
            NavDestination overlappingDestination = hierarchy.isEmpty()
                    ? newDest
                    : hierarchy.getLast().getDestination();
            // Pop any orphaned navigation graphs that don't connect to the new destinations
            //noinspection StatementWithEmptyBody
            while (!mBackStack.isEmpty()
                    && mBackStack.getLast().getDestination() instanceof NavGraph
                    && ((NavGraph) mBackStack.getLast().getDestination()).findNode(
                            overlappingDestination.getId(), false) == null
                    && popBackStackInternal(mBackStack.getLast().getDestination().getId(), true)) {
                // Keep popping
            }
            mBackStack.addAll(hierarchy);
            // The mGraph should always be on the back stack after you navigate()
            if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
                NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
                        mLifecycleOwner, mViewModel);
                mBackStack.addFirst(entry);
            }
            // And finally, add the new destination with its default args
            NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                    newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
            mBackStack.add(newBackStackEntry);
        } else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
            launchSingleTop = true;
            NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
            if (singleTopBackStackEntry != null) {
                singleTopBackStackEntry.replaceArguments(finalArgs);
            }
        }
        updateOnBackPressedCallbackEnabled();
        if (popped || newDest != null || launchSingleTop) {
            dispatchOnDestinationChanged();
        }
    }

    /**
     * Navigate via the given {@link NavDirections}
     *
     * @param directions directions that describe this navigation operation
     */
    public void navigate(@NonNull NavDirections directions) {
        navigate(directions.getActionId(), directions.getArguments());
    }

    /**
     * Navigate via the given {@link NavDirections}
     *
     * @param directions directions that describe this navigation operation
     * @param navOptions special options for this navigation operation
     */
    public void navigate(@NonNull NavDirections directions, @Nullable NavOptions navOptions) {
        navigate(directions.getActionId(), directions.getArguments(), navOptions);
    }

    /**
     * Navigate via the given {@link NavDirections}
     *
     * @param directions directions that describe this navigation operation
     * @param navigatorExtras extras to pass to the {@link Navigator}
     */
    public void navigate(@NonNull NavDirections directions,
            @NonNull Navigator.Extras navigatorExtras) {
        navigate(directions.getActionId(), directions.getArguments(), null, navigatorExtras);
    }

    /**
     * Create a deep link to a destination within this NavController.
     *
     * @return a {@link NavDeepLinkBuilder} suitable for constructing a deep link
     */
    @NonNull
    public NavDeepLinkBuilder createDeepLink() {
        return new NavDeepLinkBuilder(this);
    }

    /**
     * Saves all navigation controller state to a Bundle.
     *
     * <p>State may be restored from a bundle returned from this method by calling
     * {@link #restoreState(Bundle)}. Saving controller state is the responsibility
     * of a {@link NavHost}.</p>
     *
     * @return saved state for this controller
     */
    @CallSuper
    @Nullable
    public Bundle saveState() {
        Bundle b = null;
        ArrayList<String> navigatorNames = new ArrayList<>();
        Bundle navigatorState = new Bundle();
        for (Map.Entry<String, Navigator<? extends NavDestination>> entry :
                mNavigatorProvider.getNavigators().entrySet()) {
            String name = entry.getKey();
            Bundle savedState = entry.getValue().onSaveState();
            if (savedState != null) {
                navigatorNames.add(name);
                navigatorState.putBundle(name, savedState);
            }
        }
        if (!navigatorNames.isEmpty()) {
            b = new Bundle();
            navigatorState.putStringArrayList(KEY_NAVIGATOR_STATE_NAMES, navigatorNames);
            b.putBundle(KEY_NAVIGATOR_STATE, navigatorState);
        }
        if (!mBackStack.isEmpty()) {
            if (b == null) {
                b = new Bundle();
            }
            Parcelable[] backStack = new Parcelable[mBackStack.size()];
            int index = 0;
            for (NavBackStackEntry backStackEntry : mBackStack) {
                backStack[index++] = new NavBackStackEntryState(backStackEntry);
            }
            b.putParcelableArray(KEY_BACK_STACK, backStack);
        }
        if (mDeepLinkHandled) {
            if (b == null) {
                b = new Bundle();
            }
            b.putBoolean(KEY_DEEP_LINK_HANDLED, mDeepLinkHandled);
        }
        return b;
    }

    /**
     * Restores all navigation controller state from a bundle. This should be called before any
     * call to {@link #setGraph}.
     *
     * <p>State may be saved to a bundle by calling {@link #saveState()}.
     * Restoring controller state is the responsibility of a {@link NavHost}.</p>
     *
     * @param navState state bundle to restore
     */
    @CallSuper
    public void restoreState(@Nullable Bundle navState) {
        if (navState == null) {
            return;
        }

        navState.setClassLoader(mContext.getClassLoader());

        mNavigatorStateToRestore = navState.getBundle(KEY_NAVIGATOR_STATE);
        mBackStackToRestore = navState.getParcelableArray(KEY_BACK_STACK);
        mDeepLinkHandled = navState.getBoolean(KEY_DEEP_LINK_HANDLED);
    }

    void setLifecycleOwner(@NonNull LifecycleOwner owner) {
        mLifecycleOwner = owner;
        mLifecycleOwner.getLifecycle().addObserver(mLifecycleObserver);
    }

    void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
        if (mLifecycleOwner == null) {
            throw new IllegalStateException("You must call setLifecycleOwner() before calling "
                    + "setOnBackPressedDispatcher()");
        }
        // Remove the callback from any previous dispatcher
        mOnBackPressedCallback.remove();
        // Then add it to the new dispatcher
        dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
    }

    void enableOnBackPressed(boolean enabled) {
        mEnableOnBackPressedCallback = enabled;
        updateOnBackPressedCallbackEnabled();
    }

    private void updateOnBackPressedCallbackEnabled() {
        mOnBackPressedCallback.setEnabled(mEnableOnBackPressedCallback
                && getDestinationCountOnBackStack() > 1);
    }

    void setViewModelStore(@NonNull ViewModelStore viewModelStore) {
        if (!mBackStack.isEmpty()) {
            throw new IllegalStateException("ViewModelStore should be set before setGraph call");
        }
        mViewModel = NavControllerViewModel.getInstance(viewModelStore);
    }

    /**
     * Gets the {@link ViewModelStoreOwner} for a NavGraph. This can be passed to
     * {@link androidx.lifecycle.ViewModelProvider} to retrieve a ViewModel that is scoped
     * to the navigation graph - it will be cleared when the navigation graph is popped off
     * the back stack.
     *
     * @param navGraphId ID of a NavGraph that exists on the back stack
     * @throws IllegalStateException if called before the {@link NavHost} has called
     * {@link NavHostController#setViewModelStore}.
     * @throws IllegalArgumentException if the NavGraph is not on the back stack
     */
    @NonNull
    public ViewModelStoreOwner getViewModelStoreOwner(@IdRes int navGraphId) {
        if (mViewModel == null) {
            throw new IllegalStateException("You must call setViewModelStore() before calling "
                    + "getViewModelStoreOwner().");
        }
        NavBackStackEntry lastFromBackStack = getBackStackEntry(navGraphId);
        if (!(lastFromBackStack.getDestination() instanceof NavGraph)) {
            throw new IllegalArgumentException("No NavGraph with ID " + navGraphId
                    + " is on the NavController's back stack");
        }
        return lastFromBackStack;
    }

    /**
     * Gets the topmost {@link NavBackStackEntry} for a destination id.
     * <p>
     * This is always safe to use with {@link #getCurrentDestination() the current destination} or
     * {@link NavDestination#getParent() its parent} or grandparent navigation graphs as these
     * destinations are guaranteed to be on the back stack.
     *
     * @param destinationId ID of a destination that exists on the back stack
     * @throws IllegalArgumentException if the destination is not on the back stack
     */
    @NonNull
    public NavBackStackEntry getBackStackEntry(@IdRes int destinationId) {
        NavBackStackEntry lastFromBackStack = null;
        Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
        while (iterator.hasNext()) {
            NavBackStackEntry entry = iterator.next();
            NavDestination destination = entry.getDestination();
            if (destination.getId() == destinationId) {
                lastFromBackStack = entry;
                break;
            }
        }
        if (lastFromBackStack == null) {
            throw new IllegalArgumentException("No destination with ID " + destinationId
                    + " is on the NavController's back stack. The current destination is "
                    + getCurrentDestination());
        }
        return lastFromBackStack;
    }

    /**
     * Gets the topmost {@link NavBackStackEntry}.
     *
     * @return the topmost entry on the back stack or null if the back stack is empty
     */
    @Nullable
    public NavBackStackEntry getCurrentBackStackEntry() {
        if (mBackStack.isEmpty()) {
            return null;
        } else {
            return mBackStack.getLast();
        }
    }

    /**
     * Gets the previous visible {@link NavBackStackEntry}.
     * <p>
     * This skips over any {@link NavBackStackEntry} that is associated with a {@link NavGraph}.
     *
     * @return the previous visible entry on the back stack or null if the back stack has less
     * than two visible entries
     */
    @Nullable
    public NavBackStackEntry getPreviousBackStackEntry() {
        Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
        // throw the topmost destination away.
        if (iterator.hasNext()) {
            iterator.next();
        }
        while (iterator.hasNext()) {
            NavBackStackEntry entry = iterator.next();
            if (!(entry.getDestination() instanceof NavGraph)) {
                return entry;
            }
        }
        return null;
    }
}