Tomáš Repčík - 15. 10. 2024

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:

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 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!

Subscribe for more
LinkedIn GitHub Medium