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