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