blob: 8a5ba6a794ffd584f30b1d8f074cd3391dc9eff7 [file] [log] [blame]
/*
* 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.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 final 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));
}
@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;
}
/**
* 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) {
TaskStackBuilder parentIntents = new NavDeepLinkBuilder(this)
.setDestination(parent.getId())
.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);
}
}
// 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) {
throw new IllegalStateException("unknown destination during restore: "
+ mContext.getResources().getResourceName(state.getDestinationId()));
}
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);
}
}
}
/**
* 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 added via {@link NavDestination#addDeepLink(String)}.</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(String)
*/
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(intent.getData());
if (matchingDeepLink != null) {
deepLink = matchingDeepLink.getDestination().buildDeepLinkIds();
bundle.putAll(matchingDeepLink.getMatchingArgs());
}
}
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) {
throw new IllegalStateException("unknown destination during deep link: "
+ NavDestination.getDisplayName(mContext, destinationId));
}
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) {
throw new IllegalStateException("unknown destination during deep link: "
+ 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());
}
} 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 : 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() {
if (mBackStack.isEmpty()) {
return null;
} else {
return mBackStack.getLast().getDestination();
}
}
@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);
throw new IllegalArgumentException("navigation destination " + dest
+ (navAction != null
? " referenced from action " + NavDestination.getDisplayName(mContext, resId)
: "")
+ " is unknown to this NavController");
}
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
*/
public void navigate(@NonNull Uri deepLink) {
navigate(deepLink, 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
*/
public void navigate(@NonNull Uri deepLink, @Nullable NavOptions navOptions) {
navigate(deepLink, navOptions, 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
* @param navigatorExtras extras to pass to the Navigator
*/
public void navigate(@NonNull Uri deepLink, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
NavDestination.DeepLinkMatch deepLinkMatch = mGraph.matchDeepLink(deepLink);
if (deepLinkMatch != null) {
Bundle args = deepLinkMatch.getMatchingArgs();
NavDestination node = deepLinkMatch.getDestination();
navigate(node, args, navOptions, navigatorExtras);
} else {
throw new IllegalArgumentException("navigation destination with deepLink "
+ deepLink + " is unknown to this NavController");
}
}
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = 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
}
}
// The mGraph should always be on the back stack after you navigate()
if (mBackStack.isEmpty()) {
NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
mLifecycleOwner, mViewModel);
mBackStack.add(entry);
}
// Now ensure all intermediate NavGraphs are put on the back stack
// to ensure that global actions work.
ArrayDeque<NavBackStackEntry> hierarchy = new ArrayDeque<>();
NavDestination destination = newDest;
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;
}
mBackStack.addAll(hierarchy);
// And finally, add the new destination with its default args
NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
mBackStack.add(newBackStackEntry);
}
updateOnBackPressedCallbackEnabled();
if (popped || newDest != null) {
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");
}
return lastFromBackStack;
}
}