1/*
2 * Copyright (C) 2012 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 android.support.v4.app;
18
19import android.app.Activity;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.pm.ActivityInfo;
24import android.content.pm.PackageManager;
25import android.content.pm.PackageManager.NameNotFoundException;
26import android.support.annotation.Nullable;
27import android.support.v4.content.IntentCompat;
28import android.util.Log;
29
30/**
31 * NavUtils provides helper functionality for applications implementing
32 * recommended Android UI navigation patterns. For information about recommended
33 * navigation patterns see
34 * <a href="{@docRoot}guide/topics/fundamentals/tasks-and-back-stack.html">Tasks and Back Stack</a>
35 * from the developer guide and <a href="{@docRoot}design/patterns/navigation.html">Navigation</a>
36 * from the design guide.
37 */
38public class NavUtils {
39    private static final String TAG = "NavUtils";
40    public static final String PARENT_ACTIVITY = "android.support.PARENT_ACTIVITY";
41
42    interface NavUtilsImpl {
43        Intent getParentActivityIntent(Activity activity);
44        boolean shouldUpRecreateTask(Activity activity, Intent targetIntent);
45        void navigateUpTo(Activity activity, Intent upIntent);
46        String getParentActivityName(Context context, ActivityInfo info);
47    }
48
49    static class NavUtilsImplBase implements NavUtilsImpl {
50
51        @Override
52        public Intent getParentActivityIntent(Activity activity) {
53            String parentName = NavUtils.getParentActivityName(activity);
54            if (parentName == null) return null;
55
56            // If the parent itself has no parent, generate a main activity intent.
57            final ComponentName target = new ComponentName(activity, parentName);
58            try {
59                final String grandparent = NavUtils.getParentActivityName(activity, target);
60                final Intent parentIntent = grandparent == null
61                        ? IntentCompat.makeMainActivity(target)
62                        : new Intent().setComponent(target);
63                return parentIntent;
64            } catch (NameNotFoundException e) {
65                Log.e(TAG, "getParentActivityIntent: bad parentActivityName '" + parentName +
66                        "' in manifest");
67                return null;
68            }
69        }
70
71        @Override
72        public boolean shouldUpRecreateTask(Activity activity, Intent targetIntent) {
73            String action = activity.getIntent().getAction();
74            return action != null && !action.equals(Intent.ACTION_MAIN);
75        }
76
77        @Override
78        public void navigateUpTo(Activity activity, Intent upIntent) {
79            upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
80            activity.startActivity(upIntent);
81            activity.finish();
82        }
83
84        @Override
85        public String getParentActivityName(Context context, ActivityInfo info) {
86            if (info.metaData == null) return null;
87            String parentActivity = info.metaData.getString(PARENT_ACTIVITY);
88            if (parentActivity == null) return null;
89            if (parentActivity.charAt(0) == '.') {
90                parentActivity = context.getPackageName() + parentActivity;
91            }
92            return parentActivity;
93        }
94    }
95
96    static class NavUtilsImplJB extends NavUtilsImplBase {
97
98        @Override
99        public Intent getParentActivityIntent(Activity activity) {
100            // Prefer the "real" JB definition if available,
101            // else fall back to the meta-data element.
102            Intent result = NavUtilsJB.getParentActivityIntent(activity);
103            if (result == null) {
104                result = superGetParentActivityIntent(activity);
105            }
106            return result;
107        }
108
109        Intent superGetParentActivityIntent(Activity activity) {
110            return super.getParentActivityIntent(activity);
111        }
112
113        @Override
114        public boolean shouldUpRecreateTask(Activity activity, Intent targetIntent) {
115            return NavUtilsJB.shouldUpRecreateTask(activity, targetIntent);
116        }
117
118        @Override
119        public void navigateUpTo(Activity activity, Intent upIntent) {
120            NavUtilsJB.navigateUpTo(activity, upIntent);
121        }
122
123        @Override
124        public String getParentActivityName(Context context, ActivityInfo info) {
125            String result = NavUtilsJB.getParentActivityName(info);
126            if (result == null) {
127                result = super.getParentActivityName(context, info);
128            }
129            return result;
130        }
131    }
132
133    private static final NavUtilsImpl IMPL;
134
135    static {
136        final int version = android.os.Build.VERSION.SDK_INT;
137        if (version >= 16) {
138            IMPL = new NavUtilsImplJB();
139        } else {
140            IMPL = new NavUtilsImplBase();
141        }
142    }
143
144    /**
145     * Returns true if sourceActivity should recreate the task when navigating 'up'
146     * by using targetIntent.
147     *
148     * <p>If this method returns false the app can trivially call
149     * {@link #navigateUpTo(Activity, Intent)} using the same parameters to correctly perform
150     * up navigation. If this method returns true, the app should synthesize a new task stack
151     * by using {@link TaskStackBuilder} or another similar mechanism to perform up navigation.</p>
152     *
153     * @param sourceActivity The current activity from which the user is attempting to navigate up
154     * @param targetIntent An intent representing the target destination for up navigation
155     * @return true if navigating up should recreate a new task stack, false if the same task
156     *         should be used for the destination
157     */
158    public static boolean shouldUpRecreateTask(Activity sourceActivity, Intent targetIntent) {
159        return IMPL.shouldUpRecreateTask(sourceActivity, targetIntent);
160    }
161
162    /**
163     * Convenience method that is equivalent to calling
164     * <code>{@link #navigateUpTo(Activity, Intent) navigateUpTo}(sourceActivity,
165     * {@link #getParentActivityIntent(Activity) getParentActivityIntent} (sourceActivity))</code>.
166     * sourceActivity will be finished by this call.
167     *
168     * <p><em>Note:</em> This method should only be used when sourceActivity and the corresponding
169     * parent are within the same task. If up navigation should cross tasks in some cases, see
170     * {@link #shouldUpRecreateTask(Activity, Intent)}.</p>
171     *
172     * @param sourceActivity The current activity from which the user is attempting to navigate up
173     */
174    public static void navigateUpFromSameTask(Activity sourceActivity) {
175        Intent upIntent = getParentActivityIntent(sourceActivity);
176
177        if (upIntent == null) {
178            throw new IllegalArgumentException("Activity " +
179                    sourceActivity.getClass().getSimpleName() +
180                    " does not have a parent activity name specified." +
181                    " (Did you forget to add the android.support.PARENT_ACTIVITY <meta-data> " +
182                    " element in your manifest?)");
183        }
184
185        navigateUpTo(sourceActivity, upIntent);
186    }
187
188    /**
189     * Navigate from sourceActivity to the activity specified by upIntent, finishing sourceActivity
190     * in the process. upIntent will have the flag {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} set
191     * by this method, along with any others required for proper up navigation as outlined
192     * in the Android Design Guide.
193     *
194     * <p>This method should be used when performing up navigation from within the same task
195     * as the destination. If up navigation should cross tasks in some cases, see
196     * {@link #shouldUpRecreateTask(Activity, Intent)}.</p>
197     *
198     * @param sourceActivity The current activity from which the user is attempting to navigate up
199     * @param upIntent An intent representing the target destination for up navigation
200     */
201    public static void navigateUpTo(Activity sourceActivity, Intent upIntent) {
202        IMPL.navigateUpTo(sourceActivity, upIntent);
203    }
204
205    /**
206     * Obtain an {@link Intent} that will launch an explicit target activity
207     * specified by sourceActivity's {@link #PARENT_ACTIVITY} &lt;meta-data&gt;
208     * element in the application's manifest. If the device is running
209     * Jellybean or newer, the android:parentActivityName attribute will be preferred
210     * if it is present.
211     *
212     * @param sourceActivity Activity to fetch a parent intent for
213     * @return a new Intent targeting the defined parent activity of sourceActivity
214     */
215    public static Intent getParentActivityIntent(Activity sourceActivity) {
216        return IMPL.getParentActivityIntent(sourceActivity);
217    }
218
219    /**
220     * Obtain an {@link Intent} that will launch an explicit target activity
221     * specified by sourceActivityClass's {@link #PARENT_ACTIVITY} &lt;meta-data&gt;
222     * element in the application's manifest.
223     *
224     * @param context Context for looking up the activity component for sourceActivityClass
225     * @param sourceActivityClass {@link java.lang.Class} object for an Activity class
226     * @return a new Intent targeting the defined parent activity of sourceActivity
227     * @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid
228     */
229    public static Intent getParentActivityIntent(Context context, Class<?> sourceActivityClass)
230            throws NameNotFoundException {
231        String parentActivity = getParentActivityName(context,
232                new ComponentName(context, sourceActivityClass));
233        if (parentActivity == null) return null;
234
235        // If the parent itself has no parent, generate a main activity intent.
236        final ComponentName target = new ComponentName(context, parentActivity);
237        final String grandparent = getParentActivityName(context, target);
238        final Intent parentIntent = grandparent == null
239                ? IntentCompat.makeMainActivity(target)
240                : new Intent().setComponent(target);
241        return parentIntent;
242    }
243
244    /**
245     * Obtain an {@link Intent} that will launch an explicit target activity
246     * specified by sourceActivityClass's {@link #PARENT_ACTIVITY} &lt;meta-data&gt;
247     * element in the application's manifest.
248     *
249     * @param context Context for looking up the activity component for the source activity
250     * @param componentName ComponentName for the source Activity
251     * @return a new Intent targeting the defined parent activity of sourceActivity
252     * @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid
253     */
254    public static Intent getParentActivityIntent(Context context, ComponentName componentName)
255            throws NameNotFoundException {
256        String parentActivity = getParentActivityName(context, componentName);
257        if (parentActivity == null) return null;
258
259        // If the parent itself has no parent, generate a main activity intent.
260        final ComponentName target = new ComponentName(
261                componentName.getPackageName(), parentActivity);
262        final String grandparent = getParentActivityName(context, target);
263        final Intent parentIntent = grandparent == null
264                ? IntentCompat.makeMainActivity(target)
265                : new Intent().setComponent(target);
266        return parentIntent;
267    }
268
269    /**
270     * Return the fully qualified class name of sourceActivity's parent activity as specified by
271     * a {@link #PARENT_ACTIVITY} &lt;meta-data&gt; element within the activity element in
272     * the application's manifest.
273     *
274     * @param sourceActivity Activity to fetch a parent class name for
275     * @return The fully qualified class name of sourceActivity's parent activity or null if
276     *         it was not specified
277     */
278    @Nullable
279    public static String getParentActivityName(Activity sourceActivity) {
280        try {
281            return getParentActivityName(sourceActivity, sourceActivity.getComponentName());
282        } catch (NameNotFoundException e) {
283            // Component name of supplied activity does not exist...?
284            throw new IllegalArgumentException(e);
285        }
286    }
287    /**
288     * Return the fully qualified class name of a source activity's parent activity as specified by
289     * a {@link #PARENT_ACTIVITY} &lt;meta-data&gt; element within the activity element in
290     * the application's manifest. The source activity is provided by componentName.
291     *
292     * @param context Context for looking up the activity component for the source activity
293     * @param componentName ComponentName for the source Activity
294     * @return The fully qualified class name of sourceActivity's parent activity or null if
295     *         it was not specified
296     */
297    @Nullable
298    public static String getParentActivityName(Context context, ComponentName componentName)
299            throws NameNotFoundException {
300        PackageManager pm = context.getPackageManager();
301        ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA);
302        String parentActivity = IMPL.getParentActivityName(context, info);
303        return parentActivity;
304    }
305
306    /** No instances! */
307    private NavUtils() {
308    }
309}
310