Analyzing App Startup and Shutdown in Android 15: New Update
Using Android 15 new ApplicationStartInfo
Android 11 introduced ApplicationExitInfo
, from which you can get historical reasons why the app was turned off / killed.
Android 15 implemented new ApplicationStartInfo
can be used to analyze the causes of why and how the app was launched.
With both kinds of data, you can track how the users use the app. You can check if no issues are persisting in the app with exit data and now also how the app is started.
ApplicationStartInfo
As it was outlined, the ApplicationStartInfo
was introduced with Android 15, so it will be available only for devices running API 35 and higher. So make sure to use appropriate version handling or mark all the methods with @RequiresApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)
.
To access history start data, you need to do the following:
val activityManager: ActivityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val startInfos = activityManager.getHistoricalProcessStartReasons(maxNumberOfInstances)
Get ActivityManager
from system services and with its help, you can pull as many historical data as they are available at storage with your maximum number of required instances.
Available data:
- Start reason - the reason why the app started - notification, user clicked on launcher icon, alarm, …
- Startup state - the start-up state at which the app is currently - error, started, first frame
- Start type - the starting state of the app - cold boot, warm boot, …
- Launch mode - how the current activity is reused or replaced - singleInstance, singleTask, singleTop, standard, …
- Forced stopped - if the launch is the first one, since the app was forced stopped
- Intent - intent used to launch the activity
- Startup Times - map of timestamps for events during the startup in nanoseconds
val startType = startInfo.startType
val startReason = startInfo.reason
val startUpState = startInfo.startupState
val launchMode = startInfo.launchMode
val startTimestamp = startInfo.startupTimestamps
val wasForceStopped = startInfo.wasForceStopped()
val startIntent = startInfo.intent
Most of the values are integers, which are defined as static integer constants under ApplicationStartInfo
.
Add callback
You can add a callback function to receive info when the app is fully started up and receive startup info right away without querying the history. It returns a list of ApplicationStartInfo
.
val executor = context.mainExecutor
activityManager.addApplicationStartInfoCompletionListener(executor) {
// access the ApplicationStartInfo via `it`
}
Add your events
You can add your events to the history with the use of your set of constants, which must be bigger than 20 and less or equal to 30 based on the documentation.
Time is in nanoseconds.
val currentTimeInNanos = System.nanoTime()
activityManager.addStartInfoTimestamp(25, timestamp)
System.nanoTime()
does not reference Unix epoch, but if you have 2 or more timestamps, it gives you more granular view on performance.
ApplicationExitInfo
To make this article complete, Android provides a way to receive information about how the app was ended. It returns a list of ApplicationExitInfo
.
val exitInfos = activityManager.getHistoricalProcessExitReasons(packageName, 0, maxNum)
It works almost in the same way, but you need to provide the package name from context, and process ID (PID), which you can leave 0 for all records and a maximum number of exit reasons to receive.
Available data:
- Exit reason - why the app was stopped
- Description - human readable reason, why the app stopped
- Exit Importance - how important the app was, when it was stopped - foreground, dozing, …
- Stop time - timestamp in milliseconds, when the app was stopped
Exit reasons are once again mostly static constant integers defined in ApplicationExitInfo
and ActivityManager.RunningAppProcessInfo
.
Example usage
Most of the states and reasons are integers defined in appropriate classes as static constants, so we usually want to use when
statement to classify the result.
I prefer using enums since integers do not define constraints and statements cannot be exhaustive. Moreover, you can still nicely serialize them with the use of Kotlin’s serialization.
Example for starting reason:
enum class StartReason {
START_REASON_ALARM,
START_REASON_BACKUP,
START_REASON_BOOT_COMPLETE,
START_REASON_BROADCAST,
START_REASON_CONTENT_PROVIDER,
START_REASON_JOB,
START_REASON_LAUNCHER,
START_REASON_LAUNCHER_RECENTS,
START_REASON_OTHER,
START_REASON_PUSH,
START_REASON_SERVICE,
START_REASON_START_ACTIVITY;
companion object {
fun fromValue(value: Int): StartReason = when (value) {
ApplicationStartInfo.START_REASON_ALARM -> START_REASON_ALARM
ApplicationStartInfo.START_REASON_BACKUP -> START_REASON_BACKUP
ApplicationStartInfo.START_REASON_BOOT_COMPLETE -> START_REASON_BOOT_COMPLETE
ApplicationStartInfo.START_REASON_BROADCAST -> START_REASON_BROADCAST
ApplicationStartInfo.START_REASON_CONTENT_PROVIDER -> START_REASON_CONTENT_PROVIDER
ApplicationStartInfo.START_REASON_JOB -> START_REASON_JOB
ApplicationStartInfo.START_REASON_LAUNCHER -> START_REASON_LAUNCHER
ApplicationStartInfo.START_REASON_LAUNCHER_RECENTS -> START_REASON_LAUNCHER_RECENTS
ApplicationStartInfo.START_REASON_OTHER -> START_REASON_OTHER
ApplicationStartInfo.START_REASON_PUSH -> START_REASON_PUSH
ApplicationStartInfo.START_REASON_SERVICE -> START_REASON_SERVICE
ApplicationStartInfo.START_REASON_START_ACTIVITY -> START_REASON_START_ACTIVITY
else -> throw IllegalArgumentException("Unknown start reason value: $value")
}
}
}
Similarly, we can create a data class to map timestamps from starting information:
data class StartupTimestamps(
val applicationOnCreate: Long? = null,
val bindApplication: Long? = null,
val firstFrame: Long? = null,
val fork: Long? = null,
val fullyDrawn: Long? = null,
val initialRenderThreadFrame: Long? = null,
val launch: Long? = null,
val reservedRangeDeveloper: Long? = null,
val reservedRangeDeveloperStart: Long? = null,
val reservedRangeSystem: Long? = null,
val surfaceFlingerCompositionComplete: Long? = null
) {
companion object {
fun fromMap(timestampMap: Map<Int, Long>): StartupTimestamps = StartupTimestamps(
applicationOnCreate = timestampMap[ApplicationStartInfo.START_TIMESTAMP_APPLICATION_ONCREATE],
bindApplication = timestampMap[ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION],
firstFrame = timestampMap[ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME],
fork = timestampMap[ApplicationStartInfo.START_TIMESTAMP_FORK],
fullyDrawn = timestampMap[ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN],
initialRenderThreadFrame = timestampMap[ApplicationStartInfo.START_TIMESTAMP_INITIAL_RENDERTHREAD_FRAME],
launch = timestampMap[ApplicationStartInfo.START_TIMESTAMP_LAUNCH],
reservedRangeDeveloper = timestampMap[ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER],
reservedRangeDeveloperStart = timestampMap[ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START],
reservedRangeSystem = timestampMap[ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_SYSTEM],
surfaceFlingerCompositionComplete = timestampMap[ApplicationStartInfo.START_TIMESTAMP_SURFACEFLINGER_COMPOSITION_COMPLETE]
)
}
}
Or we can define exit reasons:
enum class ExitReason {
REASON_ANR,
REASON_CRASH,
REASON_CRASH_NATIVE,
REASON_DEPENDENCY_DIED,
REASON_EXCESSIVE_RESOURCE_USAGE,
REASON_EXIT_SELF,
REASON_FREEZER,
REASON_INITIALIZATION_FAILURE,
REASON_LOW_MEMORY,
REASON_OTHER,
REASON_PACKAGE_STATE_CHANGE,
REASON_PACKAGE_UPDATED,
REASON_PERMISSION_CHANGE,
REASON_SIGNALED,
REASON_UNKNOWN,
REASON_USER_REQUESTED,
REASON_USER_STOPPED;
companion object {
fun fromValue(value: Int): ExitReason = when (value) {
ApplicationExitInfo.REASON_ANR -> REASON_ANR
ApplicationExitInfo.REASON_CRASH -> REASON_CRASH
ApplicationExitInfo.REASON_CRASH_NATIVE -> REASON_CRASH_NATIVE
ApplicationExitInfo.REASON_DEPENDENCY_DIED -> REASON_DEPENDENCY_DIED
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> REASON_EXCESSIVE_RESOURCE_USAGE
ApplicationExitInfo.REASON_EXIT_SELF -> REASON_EXIT_SELF
ApplicationExitInfo.REASON_FREEZER -> REASON_FREEZER
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> REASON_INITIALIZATION_FAILURE
ApplicationExitInfo.REASON_LOW_MEMORY -> REASON_LOW_MEMORY
ApplicationExitInfo.REASON_OTHER -> REASON_OTHER
ApplicationExitInfo.REASON_PACKAGE_STATE_CHANGE -> REASON_PACKAGE_STATE_CHANGE
ApplicationExitInfo.REASON_PACKAGE_UPDATED -> REASON_PACKAGE_UPDATED
ApplicationExitInfo.REASON_PERMISSION_CHANGE -> REASON_PERMISSION_CHANGE
ApplicationExitInfo.REASON_SIGNALED -> REASON_SIGNALED
ApplicationExitInfo.REASON_UNKNOWN -> REASON_UNKNOWN
ApplicationExitInfo.REASON_USER_REQUESTED -> REASON_USER_REQUESTED
ApplicationExitInfo.REASON_USER_STOPPED -> REASON_USER_STOPPED
else -> throw IllegalArgumentException("Unknown exit reason value: $value")
}
}
}
Feel free to replace the throw with some default value, if you want to.
Similarly, to other parameters, you can create further enums or other data classes to your pleasure. For further details visit the Android documentation for ApplicationStartInfo
here and ApplicationExitInfo
here.
For a full example app, visit my repository here.
Thanks for reading and remember to follow!