Finding the reason for the exit in Android Application

When working with an Android application, we might have to keep an eye on crashes or unwanted ways that the user exits the app. We use many third-party libraries to log exits in android.

Now, with Android R, we can log the exits in our app which can help us fix any issue that might exist in our app.

In this blog, we are going to talk, how we can log the exits of the user from our application.

Let' s first discuss the type of crashes and exits we might see in our application. There can be a crash for an exception or maybe there might be an ANR that might lead your app to crash and drop users out of the application. The users might also exit the app intentionally when they have completed using the app.

Consider an example, let us say we are using WhatsApp and we want to log all the actions when a user exits the app. It can be a crash or an exception, or maybe a user action to close the app.

So, to get all the information we are going to use the ActivityManager.

But before that,

What is ActivityManager?

ActivityManager gives information about and interacts with, activities, services, and the containing process. Methods in this class give us certain information that we can use to debug our application.

So, first, I will create a project with MainActivity like,

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Now, we will initialize the ActivityManager like,

val am = applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager

Now, we will use ActivityManager to get the list of historic exit reasons for the app. To get the list, we will use,

val exitList = am.getHistoricalProcessExitReasons(applicationContext.packageName, 0, 1)

Here, getHistoricalProcessExitReasons takes three parameters,

  • Package name: Here, we passed the package name of our application
  • PID: We passed 0 as default to get all the reasons
  • Size: The size of the list of exit reasons. So, we will get a list of type ApplicationExitInfo with only one item.
Here the exitList returns the history of exit, not the exit information in real-time as it provides the historical data.

Now, let's first set up the exitList to return us the cause and reason of our crashes.

In our onCreate(), I will add,

if (exitList.isNotEmpty()) {
    val lastExitInformation: ApplicationExitInfo = exitList.first()
}

Since we have only one item in the list lastExitInformation will get the value from the list using .first()

Now, we will Log the exit reasons, time and description using,

Log.d(TAG, "Reason: ${lastExitInformation.reason}")

Log.d(TAG, "Timestamp: ${lastExitInformation.timestamp}")

lastExitInformation.description?.let {
    Log.d(TAG, "Description: ${it}")
}

Here,

  • lastExitInformation will return an integer which corresponds to the reason of the crash
  • Timestamp returns Time in Epoch and,
  • The description returns the descriptive version of the crash.

Now, let's add a TextView in XML file like,

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/textView"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

and set the reason of crash to the textView using,

textView.text = lastExitInformation.reason.toString()

We are done setting up everything to get the historic exit reasons. Now, let's start logging the reasons.

Case 1:

When we close the app from the recent app's tray and then reopen the app again we would see the following log,

MainActivity: Reason: 10
MainActivity: Timestamp: 1589626919924
MainActivity: Description: remove task

Here,

  • Reason code is 10 which means, REASON_USER_REQUESTED.
  • We got timestamp as well in our log and,
  • We got the description as remove a task, as we removed the app from the recent app's tray.

We can also see the reason code in the textView.

The reason code and its meaning are,

const val REASON_ANR = 6
const val REASON_CRASH = 4
const val REASON_CRASH_NATIVE = 5
const val REASON_DEPENDENCY_DIED = 12
const val REASON_EXCESSIVE_RESOURCE_USAGE = 9
const val REASON_EXIT_SELF = 1
const val REASON_INITIALIZATION_FAILURE = 7
const val REASON_LOW_MEMORY = 3
const val REASON_OTHER = 13
const val REASON_PERMISSION_CHANGE = 8
const val REASON_SIGNALED = 2
const val REASON_UNKNOWN = 0
const val REASON_USER_REQUESTED = 10
const val REASON_USER_STOPPED = 11

So, for example, if we get a crash the reason code would be 4 according to the above table.

Case 2:

When we get a crash in our application. So, first, let's introduce a crash in our application. We will create a button using,

lateinit var button: Button

and on click of textView, we will invoke the button's click listener like,

textView.setOnClickListener {
    button.setOnClickListener {  }
}

This would lead to a crash of our application because the button is not mapped to any widget in XML.

When we reopen the app again, the app will generate a log,

MainActivity: Reason: 14
MainActivity: Timestamp: 1589627622761
MainActivity: Description: crash

Here, the reason code is 4, which maps to REASON_CRASH from the above table and the description also specifies crash.

If any exception occurs as well, the reason code would be REASON_CRASH

Case 3:

Let us terminate the JVM using System.exit().

So, In on the click of textView, we will add System.exit() like,

textView.setOnClickListener {
    System.exit(0)
}

This will terminate the app. When we reopen the app, it will generate a log like,

MainActivity: Reason: 1
MainActivity: Timestamp: 1589627880910

Here, we won't see any description but the reason code is 1 which maps to REASON_EXIT_SELF. This means it terminated itself.

Case 4:

When our application gets ANR.

So, to generate an ANR we will run an infinite loop on click of textView using,

textView.setOnClickListener {
    var a = 0
    while (true) {
        a++
    }
}

So, when App gets an ANR the Log generated would be,

MainActivity: Reason: 6
MainActivity: Timestamp: 1589628198405

Here, the reason code is 6, which means REASON_ANR.

These are a few cases where we can log our exits form our application.

In this blog, we are using the maximum size as 1. If we change it to any number more then 1, let's say maybe 10. It will return the list of last 10 exit reasons information. To set max size to 10, we update the code,

val exitList = am.getHistoricalProcessExitReasons(applicationContext.packageName, 0, 10)

This is how we can use getHistoricalProcessExitReasons to get the reasons for the exit of the app which might be very useful for developers to understand the user's behavior of using the app or maybe checking for crashes.

Happy learning.

Team MindOrks :)

Also, Let’s connect on Twitter, Linkedin, Github, and Facebook