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