Android: bug in launchMode="singleTask"? -> activity stack not preserved

Solution 1:

This is not a bug. When an existing singleTask activity is launched, all other activities above it in the stack will be destroyed.

When you press HOME and launch the activity again, ActivityManger calls an intent

{act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]flag=FLAG_ACTIVITY_NEW_TASK|FLAG_ACTIVITY_RESET_IF_NEEDED cmp=A}

So the result is A > B > HOME > A.

It's different when A's launchMode is "Standard". The task which contains A will come to the foreground and keep the state the same as before.

You can create a "Standard" activity eg. C as the launcher and startActivity(A) in the onCreate method of C

OR

Just remove the launchMode="singleTask" and set FLAG_ACTIVITY_CLEAR_TOP|FLAG_ACTIVITY_SINGLE_TOP flag whenever call an intent to A

Solution 2:

From http://developer.android.com/guide/topics/manifest/activity-element.html on singleTask

The system creates the activity at the root of a new task and routes the intent to it. However, if an instance of the activity already exists, the system routes the intent to existing instance through a call to its onNewIntent() method, rather than creating a new one.

This means when the action.MAIN and category.LAUNCHER flags targets your application from the Launcher, the system would rather route the intent to the existing ActivityA as opposed to creating a new task and setting a new ActivityA as the root. It would rather tear down all activities above existing task ActivityA lives in, and invoke it's onNewIntent().

If you want to capture both the behavior of singleTop and singleTask, create a separate "delegate" activity named SingleTaskActivity with the singleTask launchMode which simply invokes the singleTop activity in its onCreate() and then finishes itself. The singleTop activity would still have the MAIN/LAUNCHER intent-filters to continue acting as the application's main Launcher activity, but when other activities desire calling this singleTop activity it must instead invoke the SingleTaskActivity as to preserve the singleTask behavior. The intent being passed to the singleTask activity should also be carried over to the singleTop Activity, so something like the following has worked for me since I wanted to have both singleTask and singleTop launch modes.

<activity android:name=".activities.SingleTaskActivity"
              android:launchMode="singleTask"
              android:noHistory="true"/>

public class SingleTaskActivity extends Activity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent();
        intent.setClass(this, SingleTop.class);
        startActivity(intent);
    }
}

And your singleTop activity would continue having its singleTop launch mode.

    <activity
        android:name=".activities.SingleTopActivity"
        android:launchMode="singleTop"
        android:noHistory="true"/>

Good luck.

Solution 3:

Stefan, you ever find an answer to this? I put together a testcase for this and am seeing the same (perplexing) behavior...I'll paste the code below in case anyone comes along and sees something obvious:

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example" >

  <uses-sdk android:minSdkVersion="3"/>

  <application android:icon="@drawable/icon" android:label="testSingleTask">

    <activity android:name=".ActivityA"
              android:launchMode="singleTask">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>

    <activity android:name=".ActivityB"/>

  </application>
</manifest>

ActivityA.java:

public class ActivityA extends Activity implements View.OnClickListener
{
  @Override
  public void onCreate( Bundle savedInstanceState )
  {
    super.onCreate( savedInstanceState );
    setContentView( R.layout.main );
    View button = findViewById( R.id.tacos );
    button.setOnClickListener( this );
  }

  public void onClick( View view )
  {
    //Intent i = new Intent( this, ActivityB.class );
    Intent i = new Intent();
    i.setComponent( new ComponentName( this, ActivityB.class ) );
    startActivity( i );
  }
}

ActivityB.java:

public class ActivityB extends Activity
{
  @Override
  public void onCreate( Bundle savedInstanceState )
  {
    super.onCreate( savedInstanceState );
    setContentView( R.layout.layout_b );
  }
}

I tried changing minSdkVersion to no avail. This just seems to be a bug, at least according to the documentation, which states the following:

As noted above, there's never more than one instance of a "singleTask" or "singleInstance" activity, so that instance is expected to handle all new intents. A "singleInstance" activity is always at the top of the stack (since it is the only activity in the task), so it is always in position to handle the intent. However, a "singleTask" activity may or may not have other activities above it in the stack. If it does, it is not in position to handle the intent, and the intent is dropped. (Even though the intent is dropped, its arrival would have caused the task to come to the foreground, where it would remain.)

Solution 4:

I think this is the behaviour you want:

singleTask resets the stack on home press for some retarded reason that I don't understand. The solution is instead to not use singleTask and use standard or singleTop for launcher activity instead (I've only tried with singleTop to date though).

Because apps have an affinity for each other, launching an activity like this:

Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if(launchIntent!=null) {
    launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
}

will cause your activty stack to reappear as it was, without it starting a new activity upon the old one (which was my main problem before). The flags are the important ones:

FLAG_ACTIVITY_NEW_TASK Added in API level 1

If set, this activity will become the start of a new task on this history stack. A task (from the activity that started it to the next task activity) defines an atomic group of activities that the user can move to. Tasks can be moved to the foreground and background; all of the activities inside of a particular task always remain in the same order. See Tasks and Back Stack for more information about tasks.

This flag is generally used by activities that want to present a "launcher" style behavior: they give the user a list of separate things that can be done, which otherwise run completely independently of the activity launching them.

When using this flag, if a task is already running for the activity you are now starting, then a new activity will not be started; instead, the current task will simply be brought to the front of the screen with the state it was last in. See FLAG_ACTIVITY_MULTIPLE_TASK for a flag to disable this behavior.

This flag can not be used when the caller is requesting a result from the activity being launched.

And:

FLAG_ACTIVITY_RESET_TASK_IF_NEEDED Added in API level 1

If set, and this activity is either being started in a new task or bringing to the top an existing task, then it will be launched as the front door of the task. This will result in the application of any affinities needed to have that task in the proper state (either moving activities to or from it), or simply resetting that task to its initial state if needed.

Without them the launched activity will just be pushed ontop of the old stack or some other undesirable behaviour (in this case of course)

I believe the problem with not receiving the latest Intent can be solved like this (out of my head):

@Override
public void onActivityReenter (int resultCode, Intent data) {
    onNewIntent(data);
}

Try it out!