Tasks

One of the most confusing aspects of Android to deal with is the concept of tasks. Fortunately, the automatic management of tasks is almost enough to get by, without you having to do much customization. However, some developers will need to tailor how their app interacts with the task system. Understanding what is possible and how to do it is not easy. It is made even more complicated by changes to Android, from both engineering and design perspectives, over the years.

This chapter will attempt to untie the knot of knowledge surrounding Android’s task system, explaining why things are the way they are. However, there will be a few places where the knot turns a bit Gordian, and we will have to settle for more about “how” and less about “why” the task system works as it does.

Prerequisites

Understanding this chapter requires that you have read the core chapters of this book.

One sample app makes heavy use of the PackageManager system service and refers in a few places to the Launchalot sample app profiled in that chapter.

First, Some Terminology

It will be useful to establish some common definitions of terms that you will encounter, both in this chapter and in other materials that describe the task system.

Task

So, what exactly is a “task”?

The Android developer documentation describes it as:

A task is a collection of activities that users interact with when performing a certain job. The activities are arranged in a stack (the back stack), in the order in which each activity is opened.

In that sense, a task is reminiscent of a tab in a tabbed browser. As the user navigates, clicking links and submitting forms, the user advances into other Web pages. Those pages could be on the same site as they started or could be on different sites. The browser BACK button is supposed to reverse the navigation, allowing the user to return from whence they came.

Back Stack

The user perceives tasks mostly in the form of pressing the BACK button, using this to return to previous “screens” that they had been on previously.

Sometimes, BACK button processing is handled within a single activity, such as when you put a dynamic fragment onto the “back stack” via addToBackStack() on a FragmentTransaction. Or, the activity could override onBackPressed() and do special stuff in certain scenarios. Those are part of the user experience of pressing BACK. From the standpoint of the task system, though, internal consumption of the BACK button presses do not affect the task.

At the task level, the “back stack” refers to a chain of activities. This matches the behavior of Web sites, where while pressing the browser BACK button might trigger in-page behavior, usually it returns you to the previous page. Similarly, while pressing BACK on an Android device might trigger in-activity behavior, usually it triggers a call to finish() on the foreground activity and returns control to whatever had preceded it on the back stack.

Recent Tasks

In a tabbed Web browser, if we have several tabs open, we think of all of them as being “running”. Frequently, we do not really even think about the concept, any more than we might think about the state of tabs in an IDE other than the one that we are working in right now. However, if you have ever had some browser tab all of a sudden start playing audio, such as from a reloaded page pulling in an audio-enabled ad banner, you are well aware that tabs are “running”, while you are also “running” to try to figure out what tab is playing the audio so you can get rid of it.

However, that is the behavior on a desktop Web browser. A desktop Web browser is not subject to heap size limitations the way Android apps are. And, historically, mobile devices had less system RAM than did their desktop and notebook counterparts, though that is rapidly changing.

In Android, therefore, developers are used to the notion that their processes may be terminated, while in the background, to free up memory for other processes. This is being done to allow for more apps to deliver more value in less system RAM.

However, from a multitasking standpoint, having apps just up and vanish is awkward. Hence, Android has the notion of “recent tasks”. These are tasks, with their corresponding back stacks, that the user has been in “recently”. How far back “recently” goes depends a bit on the version of Android – there could be as few as eight items. These “recent tasks” may or may not have a currently-running process associated with them. However, if the user chooses to return to one of those recent tasks, and there is no process for it, Android will seamlessly fork a fresh process, to be able to not only start up those apps, but return the user to where they were, in terms of UI contents (e.g., saved instance state Bundle) and in terms of back stack contents (e.g., where the user goes if the user now presses BACK).

Overview Screen

In a tabbed Web browser, you can navigate between different tabs in some browser-specific way. Some tabs may have the actual “tab” visible around the address bar. Some tabs might only be reachable via some sort of scrolling operation, or via a drop-down list, for people who have lots and lots of tabs open. Regardless, there is some UI means to pick the tab that you want to be viewing in the main browser area.

In Android, the “overview screen” is where the user can view the recent tasks and choose to return to one of them. Many people, including this author, refer to this as the “recent tasks list”, but apparently the official term is “overview screen”.

The way the overview screen has looked and worked has changed over the years.

Android 1.x/2.x

In the early days of Android, long-pressing the HOME button would bring up the overview screen, with up to eight recent tasks:

Overview Screen, from Android 2.3.3
Figure 648: Overview Screen, from Android 2.3.3

And… that was pretty much it.

Android 3.x/4.x

The overall move to the holographic theme for Android brought with us a new icon, for a dedicated way to get to the overview screen:

Overview Screen/Recent Tasks Navigation Bar Icon, from Android 4.3
Figure 649: Overview Screen/Recent Tasks Navigation Bar Icon, from Android 4.3

Devices that offered a navigation bar at the bottom would have this button. Devices that chose to have off-screen affordances for BACK and HOME might have a similar button for the overview screen. For those that neither had a navigation bar nor a dedicated off-screen button for the overview screen, long-pressing HOME would bring up the overview screen.

The overview screen could have more apps (15 or so) before old tasks would be dropped:

Overview Screen, from Android 4.3
Figure 650: Overview Screen, from Android 4.3

The overview screen also added more improvements:

Android 5.x

Functionally, the Android 5.x overview screen functions much like its 4.x counterpart, with the ability to see previews of tasks and remove tasks from the screen.

However, there are some differences, starting with the navigation bar icon used to bring up the overview screen:

Overview Screen/Recent Tasks Navigation Bar Icon, from Android 5.0
Figure 651: Overview Screen/Recent Tasks Navigation Bar Icon, from Android 5.0

Also, the previews are larger and stacked like cards, more so than being a classically vertically-scrolling list:

Overview Screen, from Android 5.0
Figure 652: Overview Screen, from Android 5.0

More importantly:

Running Tasks

A running task is a task that has running process(es) associated with it. Recent tasks may or may not be running.

And Now, a Bit About Task Killers

In October 2008, the first Android device was publicly released (the T-Mobile G1, a.k.a., HTC Dream).

Around December of 2008, the first task killers appeared on the Android Market (now the Play Store).

While the techniques used in 2008 to kill tasks were removed in later releases, some amount of task management behavior still exists in Android. Having a task killer is useful for understanding how tasks (and their killers) behave on Android. In particular, it is useful to have a way to emulate an app’s process being terminated due to low memory conditions… which is exactly what modern task killers do.

So, in this section, we will explore the concept of task killers, including how to implement one, before using this tool to help us explore the overall Android task system.

What Do Task Killers Do?

Despite the name, task killers do not kill tasks.

Rather, task killers terminate background processes. This does not impact the task, insofar as it will still be in the recent tasks roster and will still show up on the overview screen. However, the process for the app associated with the task will shut down.

Task killers can only request to terminate background processes. If your app is in the foreground (i.e., has the foreground activity), it cannot be terminated by a task killer.

To terminate background processes, task killers need to hold the KILL_BACKGROUND_PROCESSES permission, via a <uses-permission> element in their manifests. That enables them to be able to call the killBackgroundProcesses() method on ActivityManager. Supplied an application ID, killBackgroundProcesses() will terminate any background process(es) associated with that application. Normally, there will only be one such process, but if the app in question is using the android:process attribute in the manifest to have multiple processes, then all the app’s processes will be terminated.

This termination is done using the same internal mechanism that is used by the “out-of-memory killer”, which is responsible for freeing up system RAM due to low memory conditions.

Killing vs. Force-Stopping

For ordinary users, there are a few options for terminating background processes. Using a task killer, or swiping the task off the overview screen on Android 4.0+, will terminate background processes. Both use killBackgroundProcesses() (or internal equivalents).

However, users can also go into the Settings app, find the app in the list of installed apps, and click a “Force Stop” button associated with that app. On the surface, this has a similar effect to the above techniques, as the background process is terminated. However, force-stopping the app also unschedules any AlarmManager or JobScheduler events for that app, plus moves the app back into the “stopped state”, blocking manifest-registered broadcast receivers. Hence, force-stopping an app has a much larger impact than does merely using a task killer.

A few devices have manufacturer-supplied task managers (a.k.a., task killers), where stopping an app from those apps actually does a force stop behind the scenes, rather than killBackgroundProcesses(). This is not a good idea, as force-stopping an app has the aforementioned side effects. Fortunately, third-party task killers cannot force-stop apps, barring any security flaws in Android that might make this possible.

Why Use One?

Nowadays, normally, users do not need task killers. Occasionally one can be useful, to stop a background process for a poorly-written app (e.g., one that powers on GPS but fails to let go of GPS when the app moves to the background). On most modern Android devices, swiping the app off the overview screen usually suffices, and so task killers are not nearly as crucial as they were in Android 1.x/2.x, where there was no such built-in background process management solution.

For developers, the problem with swiping an app off the overview screen is that it not only terminates background processes, but it also removes the task entirely. This makes it difficult to see what the behavior is when apps’ processes terminate for more conventional reasons (e.g., out-of-memory killer) and how tasks tie into that. While developers have the ability to stop processes through development tools (e.g., the process list in DDMS), that just terminates the process, and it may do so slightly differently than does the out-of-memory killer. Hence, having a task killer around can be useful for experimentation purposes.

And, since getting a task killer on an emulator can be challenging (since emulators do not have access to the Play Store), having the source code for a simple task killer is useful for developers. So, let’s look at how to implement a task killer.

A Canary for the Task’s Coal Mine

In order to see some of the effects of fussing with our tasks, we need an app where we can see when our saved instance state comes and goes. To that end, we have the Tasks/TaskCanary sample application. It consists of a single activity, with a UI that is merely a full-screen EditText. In addition to the automatic saving of the EditText contents in the saved instance state Bundle, we also keep track of the time we first worked with that Bundle, in a data member named creationTime, backed by a STATE_CREATION_TIME entry in the Bundle itself:

package com.commonsware.android.task.canary;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends Activity {
  private static final String STATE_CREATION_TIME="creationTime";
  private long creationTime=-1L;

  @Override
  public void onCreate(Bundle state) {
    super.onCreate(state);
    setContentView(R.layout.activity_main);
  }

  @Override
  protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);

    dumpBundleToLog("restore", savedInstanceState);
    creationTime=savedInstanceState.getLong(STATE_CREATION_TIME, -1L);
  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    outState.putLong(STATE_CREATION_TIME, getCreationTime());
    dumpBundleToLog("save", outState);
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.actions, menu);

    return(super.onCreateOptionsMenu(menu));
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId()==R.id.settings) {
      startActivity(new Intent(Settings.ACTION_DATE_SETTINGS));
    }
    else if (item.getItemId()==R.id.other) {
      startActivity(new Intent(this, OtherActivity.class));
    }

    return(super.onOptionsItemSelected(item));
  }

  private long getCreationTime() {
    if (creationTime==-1L) {
      creationTime=System.currentTimeMillis();
    }

    return(creationTime);
  }

  // inspired by http://stackoverflow.com/a/14948713/115145

  private void dumpBundleToLog(String msg, Bundle b) {
    Log.d(getClass().getSimpleName(),
        String.format("Task ID #%d", getTaskId()));

    for (String key: b.keySet()) {
      Log.d(getClass().getSimpleName(),
          String.format("(%s) %s: %s", msg, key, b.get(key)));
    }
  }
}
(from Tasks/TaskCanary/app/src/main/java/com/commonsware/android/task/canary/MainActivity.java)

Each time we save and restore the instance state, we dump the Bundle to Logcat, so we can see what is in that Bundle. We wind up with lines like:


D/MainActivity: (save) android:viewHierarchyState: Bundle[...]
D/MainActivity: (save) creationTime: 1427032894794
D/MainActivity: (restore) android:viewHierarchyState: Bundle[...]
D/MainActivity: (restore) creationTime: 1427032894794

(where the ... is a bit long to reproduce in the book and is not essential for the sample)

This way, both in the UI and in the logs, we can confirm that our state is being saved and restored as expected… or perhaps not as expected, in some cases.

You will notice that we have a pair of action bar items. One will bring up a screen from the Settings app, which we will use to see how this affects our task. The other one will bring up another activity from our app, which we will use to explore how to start a clean task.

The Default User Experience

With all that behind us, let’s start talking about tasks, focusing first on what behavior the developer gets “out of the box”, with no task-specific logic in the app. In other words, what is the default user experience for an ordinary Android app?

NOTE: if you wish to reproduce the results described here, you will want to have the Task Canary installed on your device or emulator.

Starting from the Home Screen

Assume that we are “starting from scratch”. For example, the user has installed your app (or bought a device with your app pre-installed) but has never run your app before. Or, perhaps the overview screen is cleared of all tasks.

If the user taps your home screen launcher icon, not only is a process forked to run your app, but a new task is created, and your app’s task will appear in the overview screen.

(see! that wasn’t so hard!)

To reproduce this behavior:

Resuming from the Overview Screen

Eventually, the user wanders away from your app. Then, later on, the user returns to your app, by finding the task associated with your app in the overview screen and tapping upon it.

In the end, you wind up in the same state as before: you have a process for your app, and your task is still in the overview screen. How we get there depends a bit on what happened with your process, in between when you had been in the foreground and when the user taps on your task in the overview screen.

If your app’s process was still running, nothing much happens of note, other than you return to the foreground. From a state standpoint, your app would be called with onSaveInstanceState() when the user left your app, but you will not be called with onRestoreInstanceState(), because your activity was not destroyed yet. Note that this assumes that you did not undergo a configuration change (e.g., user originally was in your app in portrait, then returned to you from the overview screen while the device was in landscape). In the case of a configuration change, your activity would be destroyed and recreated by default, and you would be called with onRestoreInstanceState(), but that would be due to the configuration change more so than the use of the task and the overview screen.

To reproduce the above behavior, given that your device was in the state after the “Starting from the Home Screen” section above:

However, it is entirely possible that while your task is around that your process is terminated to free up memory for other processes. If the user returns to your app via the overview screen, a fresh process will be forked for your app. This would trigger a call to onRestoreInstanceState(), because your old activity no longer exists, because its process no longer exists.

Note that if you leave a task for an extended period of time — say, 30 minutes or so — the task may be “cleared” when you return to it. This means that you are taken back to whatever the “root” activity of the task is, where by “root” we mean the original activity put into the task.

Starting Another App

Some apps only start up other activities within the same app. However, many apps start up activities from other apps, either directly via startActivity() or indirectly (e.g., clicking on links in a WebView). For example, the Task Canary app has an item in the action bar overflow that, when clicked, brings up the Settings screen for adjusting date and time settings.

You might think that when the user taps on this overflow item, and Task Canary calls startActivity(), that a new task is created. After all, the Settings app is a completely separate app from the Task Canary app.

However, try this:

You will see one entry in the overview screen, for Task Canary, rather than two. Furthermore, particularly on Android 5.x devices, you will see the Settings screen as the top-most activity within the Task Canary task:

The Task Canary Task, on Android 5.1
Figure 653: The Task Canary Task, on Android 5.1

However, suppose that instead of using ACTION_DATE_SETTINGS for the Intent, we used ACTION_APN_SETTINGS instead, to allow the user to view mobile access point names and such. You might think, given the above flow, that we would wind up with just one task, as we did with ACTION_DATE_SETTINGS. In reality, you will see two tasks, instead of just one:

Two Tasks on Android 5.1
Figure 654: Two Tasks on Android 5.1

This is where things start to get a bit confusing.

Explaining the Default Behavior

With the user experience as background, let’s now dive into what is really going on with these operations.

When Tasks are Created

A task is not created just because an activity is started. Otherwise, even individual apps would have lots of tasks, one per activity.

A task is not created just because a task from a different app is started. Otherwise, the two Settings scenarios above would have both resulted in a new task.

Instead, tasks are created when somebody asks for a task to be created. That “somebody” could be the author of the app calling startActivity() or the author of the activity being started.

There are three major approaches for indicating that a new task should be started: flags on the Intent used with startActivity(), task affinity values, and launch modes. We will get into launch modes later in this chapter, as the normally-used launch modes have no impact on tasks. Instead, we will focus on the other two approaches here.

Task-Management Intent Flags

If you want to start an activity, and ensure that the activity starts in a new task, add Intent.FLAG_ACTIVITY_NEW_TASK to the flags on the Intent being used with startActivity():


startActivity(new Intent(SOME_ACTION_STRING)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))

What will happen, when you call startActivity() with FLAG_ACTIVITY_NEW_TASK, is that Android will see if there is a task that already has this activity in it. If there is, that task will be brought to the foreground, and the user will see whatever is on the top of that task’s stack. Otherwise, if there is no task with this activity in it, Android will create a new task and associate a new instance of the activity with this task.

This is what home screen launchers do. When you tap on a home screen launcher icon, if there is a task that has a copy of your home screen activity in it, that task is brought back to the foreground. Otherwise, a new task is started.

If you also add Intent.FLAG_ACTIVITY_MULTIPLE_TASK, then Android skips the search for existing tasks and unconditionally launches the activity into a new task. This is generally not a good idea, as the user can wind up with many copies of this activity, not know which one is which, and perhaps have difficulty getting back to the right one.

Task Affinities

By default, if Android needs to create a new task as a result of FLAG_ACTIVITY_NEW_TASK, it just creates a task. And, if there is no such flag on the Intent, Android will put the activity into the task of whoever called startActivity().

If, however, the activity has an android:taskAffinity attribute in its <activity> element in the manifest, then Android will specifically start this activity in a certain task, identified by the string value of the attribute. Other activities with the same task affinity will also go into this task.

The reason why the two Settings screens behave differently is that the ACTION_APN_SETTINGS activity has a certain task affinity value, while ACTION_DATE_SETTINGS does not. The task affinity of the ACTION_APN_SETTINGS activity is shared by many, though not all, activities within the Settings app. Those activities, when started, will always go into the task identified by the affinity. Hence, when we start ACTION_DATE_SETTINGS, it goes in our task (because that activity has no affinity and we did not include FLAG_ACTIVITY_NEW_TASK), but when we start the ACTION_APN_SETTINGS activity, it goes into a Settings-specific task.

Note that you can also have the android:taskAffinity value defined on the <application> element, to provide a default task affinity for all activities. The overall default is "", or no affinity.

When Tasks are Removed

On Android 3.0 and higher, the user can get rid of a task by swiping the task off of the overview screen.

Otherwise, prior to Android 5.0, a task would automatically go away after some amount of user activity, as there were only so many “slots” available for tasks.

On Android 5.0+, though, it is unclear if there is an upper bound to how many tasks can exist. Beyond that, tasks survive a reboot, as information about those tasks is persisted. We will get more into the ramifications of this, and how you can take advantage of it, later in this chapter.

When Tasks (and Processes) are Resumed

A task will be resumed and brought back to the foreground in several situations, including:

However, just because the task exists does not mean that the process(es) exist for the activities in the task. As needed, Android will fork fresh processes, to be able to load in the app’s code and start the necessary activities. Android will deliver to the newly-created activities the same Intent that was used to create the original incarnation of the activity (via getIntent()) and the saved instance state Bundle.

What Happens to Services

In theory, services are immune to task behavior. Tasks can come and go, and services are usually oblivious to this.

A service should be called with onTaskStopped() if a task associated with one or more of the app’s activities is removed. The service might use that as a signal that it too should shut itself down.

There appears to be a quasi-documented android:stopWithTask attribute on the <service> element in the manifest. The default is false, but if you override it to be true on your <service>, then onTaskStopped() will not be called, and Android will simply destroy your service when the task is removed.

However, as of Android 4.4, there are many reports that services may be destroyed when a task is removed, even without android:stopWithTask="true", though on a slight delay. Developers concerned about this should keep an eye on this issue and this issue, both for various hacky workarounds and for any signs that this is being permanently addressed.

What’s Up with onDestroy()?

If the user swipes away the task using the overview screen, onDestroy() will be called on all outstanding activities. If a “task killer” terminates your background process, your onDestroy() methods will not be called when your process is terminated by those apps.

So, removing a task is a graceful exit, and Android calls onDestroy(), but an explicit termination of your process by another is a not-so-graceful exit, and Android skips onDestroy().

As a result, as previously advised in this book and elsewhere, you cannot count on your onDestroy() methods being called, and you need to take this into account in terms of what sorts of code you put in them.

Basic Scenarios for Changing the Behavior

In many cases, the default behavior of tasks is just fine. However, there are many scenarios in which we may want to override the default behavior, routing activities to specific tasks, to have a better flow for the user.

Reusing an Activity

By default, each time you call startActivity(), a new instance of the activity is created. Depending upon the user flow, that may not be a bad approach. For example, it may be that the only logical path out of the started activity will be to press BACK and destroy it.

However, there will be plenty of cases where we will not want to keep creating new activity instances. For example, if you elect to have several activities reachable via a nav drawer, you do not want to create fresh instances of activities that the user has already visited via that drawer. Otherwise, they will keep piling up, continuing to consume heap space. Instead, it would be better to try to reuse an existing activity instance, if one is available, creating a fresh one only if needed.

The most flexible approach for accomplishing this involves using a flag on the Intent used to start the activity: Intent.FLAG_ACTIVITY_REORDER_TO_FRONT. This tells Android to bring an existing activity matching our Intent to the foreground, if one already exists in our task. If there is no such activity, then go ahead and create a new instance.

The Tasks/RoundRobin sample application demonstrates this. It consists of two activities (FirstActivity and SecondActivity), each of whose UI consists of one really big button. Clicking the button should start the other activity, so clicking the button in FirstActivity should start an instance of SecondActivity. But, we want to reuse activity instances where available, and confirm that indeed we are reusing those instances.

FirstActivity accomplishes that by adding FLAG_ACTIVITY_REORDER_TO_FRONT to the Intent used to start SecondActivity when the button is clicked:

package com.commonsware.android.tasks.roundrobin;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

public class FirstActivity extends Activity implements View.OnClickListener {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.first);
    findViewById(R.id.button).setOnClickListener(this);

    Log.d(getClass().getSimpleName(),
        String.format("onCreate for %x", hashCode()));
  }

  @Override
  protected void onResume() {
    super.onResume();

    Log.d(getClass().getSimpleName(),
          String.format("onResume for %x", hashCode()));
  }

  @Override
  protected void onDestroy() {
    Log.d(getClass().getSimpleName(),
        String.format("onDestroy for %x", hashCode()));

    super.onDestroy();
  }

  @Override
  public void onClick(View view) {
    startActivity(new Intent(this, SecondActivity.class)
                    .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
  }
}
(from Tasks/RoundRobin/app/src/main/java/com/commonsware/android/tasks/roundrobin/FirstActivity.java)

SecondActivity has a nearly identical implementation, just routing back to FirstActivity.

If you run the app, the user’s perspective is that clicking the button “ping-pongs” the user between the two activities. Looking at Logcat, you will see new instances created the first time the user visits an activity, courtesy of the Log.d() call in onCreate(). But, if the user returns to an existing instance via the button click, you will see that onCreate() is not called, and that the hashCode() reported in onResume() matches the hashCode() of the previously-created instance of this activity:


D/FirstActivity: onCreate for b31b9430
D/FirstActivity: onResume for b31b9430
D/SecondActivity: onCreate for b31eb8a8
D/SecondActivity: onResume for b31eb8a8
D/FirstActivity: onResume for b31b9430
D/SecondActivity: onResume for b31eb8a8

Note that launch modes offer another way to control this behavior, having the activity being started indicate that its instance should always be reused. However, this is a specialty case, one that most apps will not require.

Forcing a Clean Task

Let’s suppose that you have an app that requires in-app authentication, via some form of login screen. For example, your app’s data is held in SQLCipher for Android, and so you need the user to supply a passphrase for the database.

In the beginning, when your app is launched from the home screen, your LAUNCHER activity appears. If that is your login screen, all is good. You collect the passphrase, create your singleton instance of the SQLCipher-enabled SQLiteOpenHelper, and you can access the database.

Eventually, the user presses HOME, and time passes. Android terminates your process to free up system RAM. The user then tries returning to your existing task, such as via the overview screen. Android creates a fresh process for you and takes you to the activity on the top of that task’s back stack. But at this point, your singleton SQLiteOpenHelper is gone, and you need to collect a passphrase again.

You might think that this is purely a UI issue. Rather than collecting the passphrase in an activity, you collect it in a fragment, one that your LAUNCHER activity uses directly, and one that other activities can use via a DialogFragment. This way, you can arrange for every activity to be able to complete the re-initialization of your process and give you access to the encrypted database again.

Another approach would be to say that you want to wipe out this task and start over, routing the user back to the LAUNCHER activity for authentication.

There are two main approaches for implementing this: setting Intent flags or using android:clearTaskOnLaunch in the manifest.

Starting a Cleared Task Yourself

One way to do that is to have each activity check to see if a new task is needed (e.g., “is the SQLiteOpenHelper singleton null?”). When that situation is detected, you call startActivity() for your LAUNCHER activity, with two flags: FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_CLEAR_TASK.

For example, the Tasks/Tasksalot sample application is a straight-up clone of Launchalot with only one change of substance: using FLAG_ACTIVITY_CLEAR_TASK instead of FLAG_ACTIVITY_RESET_TASK_IF_NEEDED:

  @Override
  protected void onListItemClick(ListView l, View v,
                                 int position, long id) {
    ResolveInfo launchable=adapter.getItem(position);
    ActivityInfo activity=launchable.activityInfo;
    ComponentName name=new ComponentName(activity.applicationInfo.packageName,
                                         activity.name);
    Intent i=new Intent(Intent.ACTION_MAIN);
    
    i.addCategory(Intent.CATEGORY_LAUNCHER);
    i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                Intent.FLAG_ACTIVITY_CLEAR_TASK);
    i.setComponent(name);
    
    startActivity(i);    
  }
(from Tasks/Tasksalot/app/src/main/java/com/commonsware/android/tasksalot/MainActivity.java)

To see this in action:

At this point, you will see the TaskCanary sample app return to the screen. From the logs in Logcat, you will see it is the same task ID as before. Yet, you are seeing the FirstActivity. OtherActivity was removed from the task as part of FLAG_ACTIVITY_CLEAR_TASK processing.

This differs from what you see in a home screen, with FLAG_ACTIVITY_RESET_TASK_IF_NEEDED. If you run the same test, but rather than use Tasksalot, you tap on the “Task Canary” icon in the home screen launcher, the task will return to the foreground, but you will be taken to OtherActivity. FLAG_ACTIVITY_CLEAR_TASK always clears the task and makes the activity that you are starting up be the root of the newly-cleared task.

Always Starting a Cleared Task

Perhaps you always want to start with a cleared task, whenever the user returns to the task after having left it previously. In other words, you always want to start back at whatever your task’s root activity is, which is typically your launcher activity.

To do this, simply have android:clearTaskOnLaunch="true" on that launcher activity. Then, for any task where that activity is the root, when the user returns to the task, any other activities in the task are reparented (if applicable) or dropped.

Note, though, that this does not mean that you get a new process. Hence, any singletons you had before may or may not still be there.

So, in the authentication scenario described above, using android:clearTaskOnLaunch="true" would take the user back to your initial activity, where you can perform the authentication. However, if you detect that the SQLiteOpenHelper still exists, and therefore you do not need the user to log in again, you could switch over to showing your initial content (e.g., run a FragmentTransaction).

This is far simpler than having the detect-the-null-singleton-on-each-activity approach. However, the downside is that the user loses context. If they were six activities deep into your app, and they get interrupted by a phone call, when they come back to your app, they are back at the beginning.

Launching an App Into a New Task

A home screen launcher app, when it invokes the user’s selected activity, will use code something like this from the Launchalot sample:

  @Override
  protected void onListItemClick(ListView l, View v,
                                 int position, long id) {
    ResolveInfo launchable=adapter.getItem(position);
    ActivityInfo activity=launchable.activityInfo;
    ComponentName name=new ComponentName(activity.applicationInfo.packageName,
                                         activity.name);
    Intent i=new Intent(Intent.ACTION_MAIN);
    
    i.addCategory(Intent.CATEGORY_LAUNCHER);
    i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    i.setComponent(name);
    
    startActivity(i);    
  }
(from Introspection/Launchalot/app/src/main/java/com/commonsware/android/launchalot/Launchalot.java)

Here, we:

FLAG_ACTIVITY_NEW_TASK indicates that we want the activity being started to be the root of a new task. If there is no outstanding task for this app, a new task will be created, a new activity instance will be created, and that activity will be the root of the task. Here, “root” means that if the user presses BACK and destroys the activity, the task itself is removed and the user returns to the home screen.

However, despite its name, FLAG_ACTIVITY_NEW_TASK does not necessarily create a new task. If there is an existing task for this app containing this activity, that task is brought back to the foreground and is left intact. The activity we request is not created, let alone brought to the foreground.

That is where FLAG_ACTIVITY_RESET_TASK_IF_NEEDED comes in. It ensures that the task that is brought to the foreground is showing the requested activity. This may involve reparenting activities as well.

Another possibility, instead of FLAG_ACTIVITY_RESET_TASK_IF_NEEDED, is FLAG_ACTIVITY_MULTIPLE_TASK. This always starts a fresh task, with a fresh instance of the requested activity in the root of that task. However, this now may mean that the user has multiple tasks for the same app, which may be confusing in some circumstances. However, this also lies at the core of Android 5.0’s documents-as-tasks support and therefore may become more familiar to users over time.

The Invisible Activity

Several sample apps in this book use an “invisible activity”, one with the theme set to Theme.Translucent.NoTitleBar. These are useful in cases where something outside your app needs an activity, but you do not really have a UI that you want to display. In the case of the book samples, having a LAUNCHER activity makes it much easier for readers like you to simply run the samples from the IDE.

However, those sample apps usually do not have any other activities. The invisible activity is just there to kick-start something else, such as AlarmManager events.

However, if you have a mix of invisible and regular activities in an app, your invisible activities still wind up potentially having a visible impact.

For example, suppose that we have an ordinary Android app, with regular activities. However, we want a home screen shortcut icon to allow the user to start something in the background, such as playing music. While an app widget would allow us to control what happens when the user taps on an icon in that app widget, a home screen shortcut icon always launches an activity. So, we make the start-the-music activity invisible via Theme.Translucent.NoTitleBar.

If the user taps on that shortcut, and none of our other activities are part of a task, things proceed as expected: the music starts and the user sees nothing (other than perhaps a Toast that we show to let the user know that we are responding to their request).

But, if one or more of our activities are in some task, launching the invisible activity brings the task back to the foreground. While our invisible activity is still invisible, the user now sees whatever other activity of ours they had last been in. It is possible that this is a feature, and not a bug, for some apps. But, in other cases, we might want the invisible activity to not have this effect.

The solution: task affinity.

Your ordinary activities can use the default task affinity, or have other task affinities as needs dictate. Your invisible activity, though, would have an android:taskAffinity value that is distinct from all others, to force it into its own task. That way, when the user taps on the shortcut, the invisible activity routes to its own task. That task will not yet exist, so the invisible activity causes the task to be created. When the invisible activity calls finish() to destroy itself after kicking off the background work, the task is now empty and is removed. Since this was a new task, no existing UI would be brought back to the foreground, and since the task is removed in the end, we are “reset” for the next time the user taps on the shortcut.

Reparenting Tasks

One of the more unusual features of Android’s task system is the ability for activities to be “reparented”, or moved from one task to another. On the surface, this feels a bit odd, as if a Web page on one browser tab might magically show up in a separate browser tab, just via navigation. And, in truth, it is a specialized use case, but one that could conceivably apply to your app.

Suppose that you were writing an SMS client. You have an activity that is your message composer, where the user can type in a text message to send to somebody. You export that activity, with Intent actions like ACTION_SEND and ACTION_SENDTO. A third-party app, using one of those Intent actions, starts up your message composer activity. In the absence of a taskAffinity to stipulate otherwise, by default, your message composer activity will be in the task of the third-party app.

Now, suppose that the user fails to actually send a message, such as by pressing HOME from the third-party app’s task. Some time later, the user taps on your app’s home screen launcher icon. At this point, there are two possibilities as to what happens:

  1. You may decide that you want to have the already-running message composer activity appear, to remind the user that they were in the middle of composing a text message and failed to either send it or explicitly BACK out of the activity.
  2. You may decide that you do not care, and you are willing to ignore that outstanding message composer activity instance.

The default is option #2. If, instead, you want to offer option #1, that is where task reparenting comes into play.

On your <activity> (or on <application> to set an app-wide default), you can have android:allowTaskReparenting="true". This indicates to Android that the message composing activity, that is on some other app’s task, can move to your app’s task when that task is created.

The trigger for this “reparenting” is the task affinity. If you do not specify a task affinity for an activity, the default affinity is for a task rooted in one of your app’s activities, typically the launcher activity. In some circumstances, when a task for your app is created, Android will search through other tasks to see if there is any activity, in another task, that has an affinity for your task and allows reparenting. If there is a match, that activity is brought into your task.

The “some circumstances” mentioned in the preceding paragraph is something using two Intent flags when calling startActivity():

As it turns out, home screen launchers are supposed to use this pair of flags when they respond to the user tapping on a home screen launcher icon.

The Tasks/ReparentDemo sample Android Studio project contains a pair of applications as modules that demonstrate this effect, based on David Wasser’s epic Stack Overflow answer.

One module, app/, contains an application with two activities, where the second activity (ReparentableActivity) has android:allowTaskReparenting="true":

<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.commonsware.android.tasks.reparent">

  <application
    android:allowBackup="true"
    android:label="@string/app_name"
    android:icon="@drawable/ic_launcher">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <activity
      android:name=".ReparentableActivity"
      android:allowTaskReparenting="true">
      <intent-filter>
        <action android:name="com.commonsware.android.tasks.reparent.WHEEEEE"/>
        <category android:name="android.intent.category.DEFAULT"/>
      </intent-filter>
    </activity>
  </application>
</manifest>
(from Tasks/ReparentDemo/app/src/main/AndroidManifest.xml)

The two activities just display static messages, indicating which of those two activities you are seeing in the foreground. They also log process and task IDs to Logcat. MainActivity does that in onCreate():

package com.commonsware.android.tasks.reparent;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {
  @Override
  public void onCreate(Bundle state) {
    super.onCreate(state);
    setContentView(R.layout.main);

    Log.d(getApplicationInfo().loadLabel(getPackageManager()).toString(),
          String.format("Process ID %d, Task ID %d",
              android.os.Process.myPid(), getTaskId()));
  }
}
(from Tasks/ReparentDemo/app/src/main/java/com/commonsware/android/tasks/reparent/MainActivity.java)

ReparentableActivity logs the same information in onResume():

package com.commonsware.android.tasks.reparent;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class ReparentableActivity extends Activity {
  @Override
  public void onCreate(Bundle state) {
    super.onCreate(state);
    setContentView(R.layout.reparent);
  }

  @Override
  public void onResume() {
    super.onResume();

    Log.d(getClass().getSimpleName(),
        String.format("Process ID %d, Task ID %d",
            android.os.Process.myPid(), getTaskId()));
  }
}
(from Tasks/ReparentDemo/app/src/main/java/com/commonsware/android/tasks/reparent/ReparentableActivity.java)

The other module, app2/, contains an application with one activity, whose UI consists of one really big button. Clicking that button triggers a launch() method that calls startActivity() on an Intent identifying the ReparentableActivity from the first app:

package com.commonsware.android.tasks.reparent.app2;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

public class MainActivity extends Activity {
  @Override
  public void onCreate(Bundle state) {
    super.onCreate(state);
    setContentView(R.layout.main);

    Log.d(getApplicationInfo().loadLabel(getPackageManager()).toString(),
        String.format("Process ID %d, Task ID %d",
            android.os.Process.myPid(), getTaskId()));
  }

  public void launch(View v) {
    startActivity(new Intent("com.commonsware.android.tasks.reparent.WHEEEEE"));
  }
}
(from Tasks/ReparentDemo/app2/src/main/java/com/commonsware/android/tasks/reparent/app2/MainActivity.java)

To see this behavior in action, install both apps. If you run them straight from your IDE, you will want to clear out all relevant tasks, either by swiping them off the recent-tasks list (or by rebooting the device or emulator, if it runs Android 4.4 or lower).

Then, start up the “Reparent Demo Aux” app (from the app2/ module). Click the button, and you will see the ReparentableActivity appear. If you press HOME, bring up the recent-tasks list, and go back to this task, you will see the same ReparentableActivity. The task, however, is for “Reparent Demo Aux”.

Now, press HOME, then start up the “Reparent Demo” app (from the app/ module). Rather than seeing the MainActivity from that app, you see the ReparentableActivity instance from before. The logs will illustrate that your process ID has not changed, but that the task ID for this activity has changed, from the task ID used by the app2/ app to the task ID created for app/. The activity has been reparented.

The use of FLAG_ACTIVITY_RESET_TASK_IF_NEEDED may sound a lot like FLAG_ACTIVITY_CLEAR_TASK. The “if needed” part comes into play in two cases:

Here, by “resettable activities”, we mean:

So, if our back stack consists of activities A-B-C-D, and C was started with FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET, and we start up one of these activities (say, A) with FLAG_ACTIVITY_RESET_TASK_IF_NEEDED, and this existing task is coming back to the foreground, C and D will be cleared from the task. The user ordinarily would be taken to activity D, but instead will be taken to activity B, because C is explicitly resettable and D is higher on the back stack.

The Self-Destructing Activity

Sometimes, you only want an activity around while it is in the foreground and the user can see it. Once the user leaves the app, you no longer want that activity to exist. For example, a bank app showing bank account details might want this behavior, so that highly-sensitive information like this does not hang around. Or, you might want this for certain activities that are memory-intensive, so they release their heap space and reduce the odds of an OutOfMemoryError.

You could attempt to manage this yourself, via timely calls to finish(), but catching all the cases when finish() is needed could get troublesome.

Instead, Android has a pair of options to have no-history activities: activities that automatically finish when the user leaves them:

You can see these in action in the Tasks/NoHistory sample application. This is a near-clone of a simple two-activity app that we saw back when we first learned about how to have multiple activities.

There are only two real differences in this version of the sample app.

First, the launcher activity (MainActivity) has android:noHistory="true" on its <activity> element:

    <activity
      android:name=".MainActivity"
      android:label="@string/app_name"
      android:noHistory="true">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
(from Tasks/NoHistory/app/src/main/AndroidManifest.xml)

Second, when that activity goes to start OtherActivity, it adds FLAG_ACTIVITY_NO_HISTORY to the Intent used with startActivity():

  public void showOther(View v) {
    Intent other=new Intent(this, OtherActivity.class);

    other.putExtra(OtherActivity.EXTRA_MESSAGE,
                   getString(R.string.other));
    other.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);

    startActivity(other);
  }
(from Tasks/NoHistory/app/src/main/java/com/commonsware/android/tasks/nohistory/MainActivity.java)

Both of these inherit from the original sample’s LifecycleLoggingActivity, which just logs messages to Logcat on the major lifecycle methods. If you run the app, click the big button to go from MainActivity to OtherActivity, then switch to some other app (via the overview screen, via the home screen launcher, etc.), you will see that both activities are destroyed, even though we do not press BACK, call finish(), or do anything else ourselves to destroy them.

This has a key side-effect: you cannot combine no-history with startActivityForResult() especially well. If the activity that calls startActivityForResult() has no-history enabled (via the manifest attribute or the Intent flag), it will simply not be called with onActivityResult().

A related attribute is android:finishOnTaskLaunch. If set to true, and if the user leaves the task and returns to it, the activity is destroyed. Whereas android:noHistory removes the activity when the user leaves the activity, android:finishOnTaskLaunch only removes the activity when the user leaves the task and returns to it.

The Hidden Task

Perhaps you have a use case where you want your entire task to be hidden from the overview screen.

To do that, you can indicate that the activity that is the root of the task (e.g., your launcher activity) is to be “excluded from recents”. To do that, you can:

Note that this only matters if the activity in question is the task root (i.e., the one that started the task). Having this setting on other activities higher in the back stack will have no effect on the visibility of the task.

Also, please note that this does not eliminate the task itself. It merely hides it from the overview screen. So, for example, suppose you were to:

You will see that those task IDs are the same. So, the task is there, and we can return to that task, but the task is merely suppressed from the listing shown in the overview screen.

Dealing with the Persistent Tasks

As noted previously in this chapter, on Android 5.0+, tasks live forever, insofar as they survive a reboot. That, coupled with a seemingly-infinite roster of recent tasks — compared with rather finite lists in earlier versions of Android — means that your app usually will be brought back from an existing task on Android 5.0+.

However, there are a few key differences.

The State of Your State

For normal process termination, in between device reboots, the Bundle that we get in onSaveInstanceState() is held onto in RAM by some core OS process. Of course, on a reboot, that process is terminated along with everything else. And a Bundle can hold onto objects that, while perhaps Parcelable, are not designed to be persisted.

The default behavior is that when your task is brought back to the foreground after a reboot, only the task’s root activity is created, and that is the only activity in the task. This effectively mimics the behavior of pre-Android 5.0 versions of Android.

However, if you want to, you can control a bit more how your task behaves on a reboot.

Your <activity> element in the manifest can have a largely-undocumented android:persistableMode attribute. If you set this to persistAcrossReboots on the activity that serves as the root of your task (e.g., your launcher activity), then you will be able to override three additional methods on your Activity:

Right now, you may think that the author of this book is drunk, as we covered those methods already, far earlier in the book.

However, what API Level 21 adds, for persistAcrossReboots activities, are flavors of those methods that take two parameters: a Bundle (as normal) and a PersistableBundle. Values that you store in the latter parameter will be delivered to you when your activity is re-created as part of your task coming back to the foreground, even after a reboot.

Note that all of the above requires that you set your compileSdkVersion to 21 or higher.

PersistableBundle allows you to save int, long, double, and String values, along with arrays of each. On Android 5.1+, you can also save boolean and arrays of boolean values. Notably, you cannot put a Parcelable (or, strangely, a Serializable) in a PersistableBundle.

If your activity has persistAcrossReboots set — as does MainActivity in the Tasks/PersistentCanary sample application — you will be called both with the single-parameter and dual-parameter versions of those methods, in that order. Unless your app has a minSdkVersion of 21 or higher, you will probably wind up overriding both versions of each method, where you put stuff in the Bundle in the single-parameter method and you put stuff in the PersistableBundle in the dual-parameter method. Since versions of Android prior to 5.0 do not know about PersistableBundle or methods that take one, only the single-parameter versions of those methods will be called on those devices. If your minSdkVersion is 21 or higher, though, you could just override the dual-parameter versions of the methods and work with both Bundle and PersistableBundle as needed.

Where You Return To

Normally, if your task is in the overview screen, and the user returns to it, the user will be taken to whatever activity was at the top of the back stack.

However, if the device reboots, and the user returns to your task, what happens depends on that semi-documented persistableMode value:

So, in the case of PersistentCanary, even if you use the overflow to bring up the date-and-time Settings screen, since that activity has the default persistRootOnly value for persistableMode, only the MainActivity will be in the task after a reboot.

Documents As Tasks

Tasks used to be relatively app-centric. By and large, each app had its own task, and just one task.

Android 5.0 extended the task system to support the notion of “documents” as tasks. Now, an app may be in several tasks, with different tasks focused on different “documents” or other specific contexts.

The vision is that this would be used by:

The benefit to the user is a standard way to switch between these different contexts, by means of the overview screen. The risk is that the overview screen becomes unwieldy, choked with too many entries to sift through.

When You Should Do This

An app should open a new “document” based on some specific explicit “open” operation by the user. So, for example:

Conversely, an app should not open a new document based on pure navigation operations:

Adding a Document

You have a few options for launching an activity as a new document, indicating that it should have a separate entry on the Android 5.0+ overview screen.

android:documentLaunchMode

If you always want this activity to form the basis of a new document, add android:documentLaunchMode="always" to the <activity> element of your manifest, and you are done. Every time you start up an instance, you will get a new document.

This can be seen in the Tasks/Docs sample application, which has an EditorActivity with the aforementioned attribute:

    <activity
      android:name=".EditorActivity"
      android:documentLaunchMode="always"
      android:maxRecents="3"
      android:autoRemoveFromRecents="true"/>
(from Tasks/Docs/app/src/main/AndroidManifest.xml)

(we will cover those other new attributes shortly)

There are four possible values for android:documentLaunchMode:

Since intoExisting depends upon Uri matches, you only want to use intoExisting if you are passing Uri values into the activity when starting it. Otherwise, use always.

FLAG_ACTIVITY_NEW_DOCUMENT

To conditionally launch an activity as a new document, have its android:documentLaunchMode set to none (or missing, since that is the default), and add Intent.FLAG_ACTIVITY_NEW_DOCUMENT to the Intent that is used to start up the activity that would represent a new document. This will have the behavior akin to intoExisting for android:documentLaunchMode, meaning that Android will search for a matching document and bring it back to the foreground if the match is available.

To replicate always functionality, add both Intent.FLAG_ACTIVITY_NEW_DOCUMENT and Intent.FLAG_ACTIVITY_MULTIPLE_TASK to the Intent.

Capping the Number of Documents

By default, you can launch as many documents as you want. However, unless you get rid of the document (as will be described below), or the user gets rid of the document (by swiping it off the overview screen), your roster of documents can keep piling up. Users may get frustrated if their overview screen is flooded by entries for your app.

You can employ an automatic least-recently-used (LRU) algorithm here by adding android:maxRecents to the <activity> that is the root of the task for the document. This indicates the maximum number of entries there should be in the overview screen for that activity, where Android will remove older tasks to make way for new ones if needed.

So, in the Docs sample, android:maxRecents="3" limits the number of EditorActivity tasks to 3; if the user tries opening more than this, older ones are quietly removed.

Note that the default value for android:maxRecents is 16. Also, there is a cap, ranging from 25 to 50, depending on device RAM — you will be unable to set it higher than this.

Removing and Retaining Documents

Android’s default behavior is that the document will exist forever, or until the user swipes it off the overview screen.

It is rather unlikely that this is really the behavior that you or your users will want. Hence, you are going to want to take some steps to ensure that your documents will go away from the overview screen when they are no longer needed.

The simplest solution is to add android:autoRemoveFromRecents="true". This indicates that once the root activity is finished (e.g., the user presses BACK), the document is removed. By default, pressing BACK does not remove the document, so you need to opt into this behavior.

However, that approach assumes that it is fairly easy for the user to get back to the task’s root activity and press BACK. If you have a complex navigation of activities within the “document”, it may not be easy for the user to trigger document removal this way.

You can also forcibly get rid of the document by calling finishAndRemoveTask() yourself on an activity in the task. For example, in a tabbed Web browser, if you have a “close tab” UI element (e.g., action bar item), that could call finishAndRemoveTask() to get rid of the “document”.

Other Task-Related Activity Properties

There are other attributes that you can place on your <activity> element in the manifest that have impacts on how that activity participates with the task system.

launchMode

Occasionally, particular techniques become much too popular in Android development, courtesy of some blog posts or other resources touting them as “quick hacks” to address certain issues. The android:launchMode attribute is one of those. Most Android apps should have no need to change launchMode off of its default value of standard, or occasionally singleTop. Yet, because the Android task system is rather confusing, some developers latch onto other launch modes and use them in places where there are better, more fine-grained solutions.

That being said, let’s explore the launch modes, with the help from the fine people at Novoda. The Novoda developers released an app on the Play Store, and an accompanying GitHub repo that helps to illustrate the launch modes.

That app has four activities, one for each of the four launch modes:

The launcher activity is the standard activity. Each activity has four buttons, to start up that activity via startActivity(), by default with no particular Intent flags (though there’s a legacy options menu that allows you to play with those as well). The color-coded UI for each activity also shows a unique identifier of the activity, the task ID of the task that the activity is in, the lifecycle methods that were invoked on that instance, and a set of stacked bars designed to illustrate what should be on the back stack for that task (using some techniques of dubious reliability, but the sort of thing that should be OK for a demo app like this).

So, when we launch the app, we get a green UI for a standard activity:

Novoda Demo App, As Initially Launched
Figure 655: Novoda Demo App, As Initially Launched

singleTop

Using singleTop for the launchMode has one effect: controlling whether a new instance of the activity is created. Normally, calling startActivity() will create a new instance of the activity, unless Intent flags dictate otherwise. With singleTop, if the activity being started is already at the top of its stack, that existing instance is simply called with onNewIntent(). Otherwise, singleTop behaves as does standard.

So, if we tap the button to launch a singleTop method in the Novoda demo app, from our earlier state, we get a blue singleTop activity:

Novoda Demo App, After Starting singleTop Activity
Figure 656: Novoda Demo App, After Starting singleTop Activity

That worked just like standard. But, if we tap the same button again, we do not get a new instance of the activity. However, the transcript of lifecycle methods shows that onNewIntent() was called:

Novoda Demo App, After Starting singleTop Activity Again
Figure 657: Novoda Demo App, After Starting singleTop Activity Again

Note that you can get a similar result by including Intent.FLAG_ACTIVITY_SINGLE_TOP on a startActivity() call. Using launchMode says you always want single-top behavior; using FLAG_ACTIVITY_SINGLE_TOP says that this time you want single-top behavior.

Pressing BACK returns you to the original green standard activity, with the blue singleTop activity having been destroyed.

singleTask

A launchMode of singleTask says that this activity must always be the root activity of a task.

If the task does not have that activity, a new task is created. So, if we tap the button to launch the singleTask activity in the Novoda demo app, we get a new task (ID 978, compared to the previous 977), with an instance of the yellow singleTask activity as its root:

Novoda Demo App, After Starting singleTask Activity
Figure 658: Novoda Demo App, After Starting singleTask Activity

However, if the activity in question is already there as the root of the task, all other activities on the back stack are cleared, and we are taken to the singleTask activity again.

So, in the Novoda demo app, if after we start the singleTask activity, we tap the button to launch a standard activity or two:

Novoda Demo App, Two standard Activities After singleTask Activity
Figure 659: Novoda Demo App, Two standard Activities After singleTask Activity

…then tap the button to launch the singleTask activity, we get largely the same screen as before, just with a few more lifecycle methods logged:

Novoda Demo App, After Starting singleTask Activity Again
Figure 660: Novoda Demo App, After Starting singleTask Activity Again

It is the same task and the same instance, but with the other activities removed.

singleInstance

singleInstance works much like singleTask, except that the task will only ever hold this one activity. No other activities will be placed into the task.

So, tapping the button to start a singleInstance activity in the Novoda demo app brings up the red singleInstance UI:

Novoda Demo App, After Starting singleInstance Activity
Figure 661: Novoda Demo App, After Starting singleInstance Activity

Tapping the same button again just triggers onNewIntent() and other lifecycle methods on the same activity in the same task. If, however, you try tapping on the button for the standard activity, your activity will go to another task. Depending on when and how you try the Novoda demo app, this could be a prior task associated with our app (e.g., one you used for earlier standard tests), or it could be a new task (if you do not have any other ones). This is based on the taskAffinity of the activity being started.

In general, singleTask and singleInstance are for unusual use cases, and ordinary Android apps should have little reason to use them. Google specifically urges you not to use them:

…standard is the default mode and is appropriate for most types of activities. SingleTop is also a common and useful launch mode for many types of activities. The other modes — singleTask and singleInstance — are not appropriate for most applications, since they result in an interaction model that is likely to be unfamiliar to users and is very different from most other applications.

alwaysRetainTaskState

As noted earlier in the chapter, tasks may be cleared by Android if the user has not been in the task for some time (e.g., 30+ minutes). In these cases, the user is taken back to the root activity.

If, however, the root activity has android:alwaysRetainTaskState="true" in its manifest entry, then Android will not apply this timeout rule. So long as the task exists, its entire state will be retained and used when the user returns to the task. This is useful for tasks where there is a lot of state that the user might regret losing.

Other Task-Related Activity Methods

There are a handful of other task-related methods and such floating around the Activity class:

finishAffinity()

This calls finish() not only on the current activity, but on all activities immediately behind it on the back stack for this task that have the same taskAffinity as does the current activity. Much of the time, the activities on the stack will all share an affinity, and therefore this will frequently finish all activities in the task. If the task has a mixed set of affinities (e.g., a mix of explicitly-named affinities and other activities using the default affinity), this method would only wipe out those behind the current with a specific match.

This method is not commonly used.

finishAndRemoveTask()

This calls finish() on all activities in the task and removes the task outright.

For example, a “logout” operation might call finishAndRemoveTask() to flush the current task, then call startActivity() to launch the login activity. That login activity will wind up in a fresh task (since the current one will be removed), and the old activity instances will go away, so the user cannot somehow stumble into them when they are not yet logged in.

getTaskId()

Returns a unique integer that identifies the task the activity resides in.

This method is not commonly used.

isTaskRoot()

isTaskRoot() is a method on Activity. It will return true if this activity instance is at the root of a task, meaning that pressing BACK should remove the task and return the user to the home screen.

moveTaskToBack()

This method moves the current task to the background. What comes to the foreground is undocumented but generally seems to be the task for the home screen. Some apps use this to offer a “minimize” or “go to background” option within the app, though this is superfluous, as the task will move to the background naturally as the user navigates their device.

setTaskDescription()

For Android 5.0+, setTaskDescription() allows you to associate an ActivityManager.TaskDescription instance with your task. Here you can provide values that help drive what the task looks like on the overview screen. Specifically, you can provide the icon, title, and background color to use for the title bar over your thumbnail on the overview screen.