Home » Android » android – Up navigation broken on JellyBean?

android – Up navigation broken on JellyBean?

Posted by: admin April 23, 2020 Leave a comment

Questions:

Source code is available here: https://github.com/novemberox/NavigationTest It’s modified version of this sample: http://developer.android.com/training/implementing-navigation/ancestral.html

I’ve got three activities:

  • Main Activity just main entry to application
  • Category Activity it’s parent for Detail Activity
  • Detail Activity

Main Activity has button that opens Category Activity and Detail Activity.
Category Activty has just one button that opens Detail Activity.
And finally Detail Activity that shows some text and has up button that simulates click on ActionBar up.

My “clicking” path is:

  • open Main Activity
  • open Detail Activity
  • click “up button”
  • Category Activty should appear
  • back click moves us to Main Activity with state restored

And this is flow that is would be expecting and it’s working just fine on every Android before Jelly Bean (tested on Galaxy Nexus 4.1.1 and emulator 4.2 Google exp pack). It works even on ICS. I’m using support lib and classes like NavUtils and TaskStackBuilder, like in sample that I pointed at beginning.

On JB when I click “up button” it goes back to Main Activity with state restored correctly. I looked into source code of support library and I saw that NavUtils.navigateUpTo method calls native JB code like Activity#navigateUpTo. I tried both NavUtils#navigateUpTo() and NavUtils.navigateUpFromSameTask() with the same unsatisfactory result.

Do you have some suggestion what to do to have this nice flow?

How to&Answers:

First things first, if you are targeting devices as high as API 17 (Android 4.2), set the targetSdkVersion to 17 in your manifest. This doesn’t break support for old devices, it just makes things work properly for newer devices. Of course, that doesn’t fix your problem — it’s just good to do it.

What Should You Use?

I assume you are basing your code off of the Ancestral Navigation example on this page. You’ve used the second example:

Intent upIntent = new Intent(this, MyParentActivity.class);
if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
    // This activity is not part of the application's task, so create a new task
    // with a synthesized back stack.
    TaskStackBuilder.from(this)
        .addNextIntent(new Intent(this, MyGreatGrandParentActivity.class))
        .addNextIntent(new Intent(this, MyGrandParentActivity.class))
        .addNextIntent(upIntent)
        .startActivities();
    finish();
 } else {
     // This activity is part of the application's task, so simply
     // navigate up to the hierarchical parent activity.
     NavUtils.navigateUpTo(this, upIntent);
 }

For the behavior you want, however, you would need to replace NavUtils.navigateUpTo with:

upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(upIntent);
finish();

Why does it work on ICS and Earlier?

In general, the support library is trying to approximate the behavior introduced in later APIs. In the case of NavUtils, the support library is trying to approximate the bahavior introduced in API 16 (a.k.a. Android 4.1). For pre-API 16 platforms, NavUtils uses:

@Override
public void navigateUpTo(Activity activity, Intent upIntent) {
    upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    activity.startActivity(upIntent);
    activity.finish();
}

This looks through the back stack for an instance of the activity specified in upInent. If it finds one, it clears everything up to it and restores it. Otherwise it just launches the activity.

On API 16+ later platforms, the support library hands things off to the native call in the Activity API. According to the docs:

public boolean navigateUpTo (Intent upIntent)

Navigate from this activity to the activity specified by upIntent, finishing this activity in the process. If the activity indicated by upIntent already exists in the task’s history, this activity and all others before the indicated activity in the history stack will be finished.

If the indicated activity does not appear in the history stack, this will finish each activity in this task until the root activity of the task is reached, resulting in an “in-app home” behavior. This can be useful in apps with a complex navigation hierarchy when an activity may be reached by a path not passing through a canonical parent activity.

From that, it is unclear whether or not it will launch the activity specified in upIntent if it is not in the back stack. Reading the source code doesn’t help clarify things either. However, judging from the behavior your app is showing, it appears that it does not attempt to launch the upIntent activity.

Regardless of which implementation is right or wrong, the end result is that you want the FLAG_ACTIVITY_CLEAR_TOP behavior, not the native API 16 behavior. Unfortunately this means that you will have to duplicate the support library approximation.

Which is Correct?

Disclaimer: I don’t work for Google, so this is my best guess.

My guess is that the API 16+ behavior is the intended behavior; it’s a builtin implementation that has access to the Android internals and can do things that are not possible through the API. Pre-API 16, I don’t think it was possible to unwind the backstack in this fashion other than by using intent flags. Thus, the FLAG_ACTIVITY_CLEAR_TOP flag is the closest approximation of the API 16 behavior available to pre-API 16 platforms.

Unfortunately the result is that between the native implementation and the support library implementation there is a violation of the principle of least astonishment in your scenario. That leads me to wonder if this is an unanticipated use of the API. I.e., I wonder if Android expects that you to traverse the full navigation path to an activity, not jump directly to it.

Just in Case

Just to avoid one possible misconception, it is possible that someone might expect shouldUpRecreateTask to magically determine that the parent isn’t in the back stack and go through the process of synthesizing everything for you using the TaskStackBuilder.

However, shouldUpRecreateTask basically determines if your activity was launched directly by your app (in which case it returns false), or if it was launched from another app (in which case it returns true). From this book, the support library checks if the intent’s “action” is not ACTION_MAIN (I don’t fully understand that), whereas on API 16 platforms it performs this check based on task affinity. Still, in the case of this app, things work out to shouldUpRecreateTask returning false.

Answer:

After reading @cyfur01’s answer, I came up with very simple solution that fixes the behavior.

Override this method in your Activity (or ideally in some BaseActivity to have it fixed for all subclasses) and it should work as expected:

@Override
public boolean navigateUpTo(Intent upIntent) {
    boolean result = super.navigateUpTo(upIntent);
    if (!result) {
        TaskStackBuilder.create(this)
            .addNextIntentWithParentStack(upIntent)
            .startActivities();
    }
    return result;
}

It leaves all work to Android implementation and then just checks return value of navigateUpTo(upIntent) which is false when an instance of the indicated activity could not be found and this activity was simply finished normally. Which is exactly the case when we want to create and start the parent Activities manually. Also note we don’t have to call finish() here as it was called by the super method.