ActivityNavigator.java revision 51c1fe23339181415ed6837f64218b29f677cd59
1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package androidx.navigation; 18 19import android.app.Activity; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.ContextWrapper; 23import android.content.Intent; 24import android.content.res.TypedArray; 25import android.net.Uri; 26import android.os.Build; 27import android.os.Bundle; 28import android.support.annotation.NonNull; 29import android.support.annotation.Nullable; 30import android.text.TextUtils; 31import android.util.AttributeSet; 32 33import java.util.regex.Matcher; 34import java.util.regex.Pattern; 35 36/** 37 * ActivityNavigator implements cross-activity navigation. 38 */ 39@Navigator.Name("activity") 40public class ActivityNavigator extends Navigator<ActivityNavigator.Destination> { 41 private static final String EXTRA_NAV_SOURCE = 42 "android-support-navigation:ActivityNavigator:source"; 43 private static final String EXTRA_NAV_CURRENT = 44 "android-support-navigation:ActivityNavigator:current"; 45 46 private Context mContext; 47 private Activity mHostActivity; 48 49 public ActivityNavigator(@NonNull Context context) { 50 mContext = context; 51 while (context instanceof ContextWrapper) { 52 if (context instanceof Activity) { 53 mHostActivity = (Activity) context; 54 break; 55 } 56 context = ((ContextWrapper) context).getBaseContext(); 57 } 58 } 59 60 @NonNull 61 Context getContext() { 62 return mContext; 63 } 64 65 @NonNull 66 @Override 67 public Destination createDestination() { 68 return new Destination(this); 69 } 70 71 @Override 72 public boolean popBackStack() { 73 if (mHostActivity != null) { 74 int destId = 0; 75 final Intent intent = mHostActivity.getIntent(); 76 if (intent != null) { 77 destId = intent.getIntExtra(EXTRA_NAV_SOURCE, 0); 78 } 79 mHostActivity.finish(); 80 dispatchOnNavigatorNavigated(destId, BACK_STACK_DESTINATION_POPPED); 81 return true; 82 } 83 return false; 84 } 85 86 @SuppressWarnings("deprecation") 87 @Override 88 public void navigate(@NonNull Destination destination, @Nullable Bundle args, 89 @Nullable NavOptions navOptions) { 90 if (destination.getIntent() == null) { 91 throw new IllegalStateException("Destination " + destination.getId() 92 + " does not have an Intent set."); 93 } 94 Intent intent = new Intent(destination.getIntent()); 95 if (args != null) { 96 intent.putExtras(args); 97 String dataPattern = destination.getDataPattern(); 98 if (!TextUtils.isEmpty(dataPattern)) { 99 // Fill in the data pattern with the args to build a valid URI 100 StringBuffer data = new StringBuffer(); 101 Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}"); 102 Matcher matcher = fillInPattern.matcher(dataPattern); 103 while (matcher.find()) { 104 String argName = matcher.group(1); 105 if (args.containsKey(argName)) { 106 matcher.appendReplacement(data, ""); 107 data.append(Uri.encode(args.getString(argName))); 108 } else { 109 throw new IllegalArgumentException("Could not find " + argName + " in " 110 + args + " to fill data pattern " + dataPattern); 111 } 112 } 113 matcher.appendTail(data); 114 intent.setData(Uri.parse(data.toString())); 115 } 116 } 117 if (navOptions != null && navOptions.shouldClearTask()) { 118 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 119 } 120 if (navOptions != null && navOptions.shouldLaunchDocument() 121 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 122 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); 123 } else if (!(mContext instanceof Activity)) { 124 // If we're not launching from an Activity context we have to launch in a new task. 125 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 126 } 127 if (navOptions != null && navOptions.shouldLaunchSingleTop()) { 128 intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 129 } 130 if (mHostActivity != null) { 131 final Intent hostIntent = mHostActivity.getIntent(); 132 if (hostIntent != null) { 133 final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0); 134 if (hostCurrentId != 0) { 135 intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId); 136 } 137 } 138 } 139 final int destId = destination.getId(); 140 intent.putExtra(EXTRA_NAV_CURRENT, destId); 141 NavOptions.addPopAnimationsToIntent(intent, navOptions); 142 mContext.startActivity(intent); 143 if (navOptions != null && mHostActivity != null) { 144 int enterAnim = navOptions.getEnterAnim(); 145 int exitAnim = navOptions.getExitAnim(); 146 if (enterAnim != -1 || exitAnim != -1) { 147 enterAnim = enterAnim != -1 ? enterAnim : 0; 148 exitAnim = exitAnim != -1 ? exitAnim : 0; 149 mHostActivity.overridePendingTransition(enterAnim, exitAnim); 150 } 151 } 152 153 // You can't pop the back stack from the caller of a new Activity, 154 // so we don't add this navigator to the controller's back stack 155 dispatchOnNavigatorNavigated(destId, BACK_STACK_UNCHANGED); 156 } 157 158 /** 159 * NavDestination for activity navigation 160 */ 161 public static class Destination extends NavDestination { 162 private Intent mIntent; 163 private String mDataPattern; 164 165 /** 166 * Construct a new activity destination. This destination is not valid until you set the 167 * Intent via {@link #setIntent(Intent)} or one or more of the other set method. 168 * 169 * 170 * @param navigatorProvider The {@link NavController} which this destination 171 * will be associated with. 172 */ 173 public Destination(@NonNull NavigatorProvider navigatorProvider) { 174 this(navigatorProvider.getNavigator(ActivityNavigator.class)); 175 } 176 177 /** 178 * Construct a new activity destination. This destination is not valid until you set the 179 * Intent via {@link #setIntent(Intent)} or one or more of the other set method. 180 * 181 * @param activityNavigator The {@link ActivityNavigator} which this destination 182 * will be associated with. Generally retrieved via a 183 * {@link NavController}'s 184 * {@link NavigatorProvider#getNavigator(Class)} method. 185 */ 186 public Destination(@NonNull Navigator<? extends Destination> activityNavigator) { 187 super(activityNavigator); 188 } 189 190 @Override 191 public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) { 192 super.onInflate(context, attrs); 193 TypedArray a = context.getResources().obtainAttributes(attrs, 194 R.styleable.ActivityNavigator); 195 String cls = a.getString(R.styleable.ActivityNavigator_android_name); 196 if (!TextUtils.isEmpty(cls)) { 197 // TODO Replace with ComponentName.createRelative() when minSdkVersion is 23 198 if (cls.charAt(0) == '.') { 199 cls = context.getPackageName() + cls; 200 } 201 setComponentName(new ComponentName(context, cls)); 202 } 203 setAction(a.getString(R.styleable.ActivityNavigator_action)); 204 String data = a.getString(R.styleable.ActivityNavigator_data); 205 if (data != null) { 206 setData(Uri.parse(data)); 207 } 208 setDataPattern(a.getString(R.styleable.ActivityNavigator_dataPattern)); 209 a.recycle(); 210 } 211 212 /** 213 * Set the Intent to start when navigating to this destination. 214 * @param intent Intent to associated with this destination. 215 * @return this {@link Destination} 216 */ 217 @NonNull 218 public Destination setIntent(Intent intent) { 219 mIntent = intent; 220 return this; 221 } 222 223 /** 224 * Gets the Intent associated with this destination. 225 * @return 226 */ 227 @Nullable 228 public Intent getIntent() { 229 return mIntent; 230 } 231 232 /** 233 * Set an explicit {@link ComponentName} to navigate to. 234 * 235 * @param name The component name of the Activity to start. 236 * @return this {@link Destination} 237 */ 238 @NonNull 239 public Destination setComponentName(ComponentName name) { 240 if (mIntent == null) { 241 mIntent = new Intent(); 242 } 243 mIntent.setComponent(name); 244 return this; 245 } 246 247 /** 248 * Get the explicit {@link ComponentName} associated with this destination, if any 249 * @return 250 */ 251 @Nullable 252 public ComponentName getComponent() { 253 if (mIntent == null) { 254 return null; 255 } 256 return mIntent.getComponent(); 257 } 258 259 /** 260 * Sets the action sent when navigating to this destination. 261 * @param action The action string to use. 262 * @return this {@link Destination} 263 */ 264 @NonNull 265 public Destination setAction(String action) { 266 if (mIntent == null) { 267 mIntent = new Intent(); 268 } 269 mIntent.setAction(action); 270 return this; 271 } 272 273 /** 274 * Get the action used to start the Activity, if any 275 */ 276 @Nullable 277 public String getAction() { 278 if (mIntent == null) { 279 return null; 280 } 281 return mIntent.getAction(); 282 } 283 284 /** 285 * Sets a static data URI that is sent when navigating to this destination. 286 * 287 * <p>To use a dynamic URI that changes based on the arguments passed in when navigating, 288 * use {@link #setDataPattern(String)}, which will take precedence when arguments are 289 * present.</p> 290 * 291 * @param data A static URI that should always be used. 292 * @see #setDataPattern(String) 293 * @return this {@link Destination} 294 */ 295 @NonNull 296 public Destination setData(Uri data) { 297 if (mIntent == null) { 298 mIntent = new Intent(); 299 } 300 mIntent.setData(data); 301 return this; 302 } 303 304 /** 305 * Get the data URI used to start the Activity, if any 306 */ 307 @Nullable 308 public Uri getData() { 309 if (mIntent == null) { 310 return null; 311 } 312 return mIntent.getData(); 313 } 314 315 /** 316 * Sets a dynamic data URI pattern that is sent when navigating to this destination. 317 * 318 * <p>If a non-null arguments Bundle is present when navigating, any segments in the form 319 * <code>{argName}</code> will be replaced with a URI encoded string from the arguments.</p> 320 * @param dataPattern A URI pattern with segments in the form of <code>{argName}</code> that 321 * will be replaced with URI encoded versions of the Strings in the 322 * arguments Bundle. 323 * @see #setData 324 * @return this {@link Destination} 325 */ 326 @NonNull 327 public Destination setDataPattern(String dataPattern) { 328 mDataPattern = dataPattern; 329 return this; 330 } 331 332 /** 333 * Gets the dynamic data URI pattern, if any 334 */ 335 @Nullable 336 public String getDataPattern() { 337 return mDataPattern; 338 } 339 } 340} 341