Blog Infos
Author
Published
Topics
, , , ,
Published


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.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Meta-programming with K2 compiler plugins

Let’s see what’s possible with plugins using the new K2 compiler, FIR. This live demo session will go through possible use cases that reduce boilerplate code and make your code safer.
Watch Video

Meta-programming with K2 compiler plugins

Tadeas Kriz
Senior Kotlin Developer
Touchlab

Meta-programming with K2 compiler plugins

Tadeas Kriz
Senior Kotlin Develo ...
Touchlab

Meta-programming with K2 compiler plugins

Tadeas Kriz
Senior Kotlin Developer
Touchlab

Jobs

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 below.

https://developer.android.com/reference/android/app/ApplicationStartInfo?source=post_page—–7605369ae891——————————–

https://developer.android.com/reference/kotlin/android/app/ApplicationExitInfo?source=post_page—–7605369ae891——————————–

For a full example app, visit my repository:

https://github.com/Foxpace/android-app-start-exit-info-example?source=post_page—–7605369ae891——————————–

Thanks for reading and remember to follow!

More Android stories:

https://tomas-repcik.medium.com/list/android-development-3a15a240889a?source=post_page—–7605369ae891——————————–

This article is previoulsy published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu