What app is running right now?!?!?

What app is running right now?!?!?

If you have ever run into the scenario where you need access to the currently running application on your Android device you know that there is always a grey area on how to go about retrieving this information. There is the now deprecated way of grabbing the running tasks from ActivityManager. What has replaced the usage statistics that once used to be managed and controlled by ActivityManager, is now migrated and confined to UsageStatsManager; and has provided us a more more granular and explicity querying system for what we are looking for without the need to do any crazy looping or unneccessary array generation to pull off one entry.

The old (deprecated) way

1val am = getSystemService(ActivityManager::class.java)
2try {
3 // The first in the list of RunningTasks is always the foreground task.
4 val foregroundTaskInfo = am.getRunningTasks(1)[0]
5 // handle foreground app
6} catch (e: SecurityException) {
7 e.printStackTrace()
8}

In the ActivityManager method the GET_TASKS permission is required to be defined in the AndroidManifest (as well as granted by the user on Android 6.0 (API 23+) - but this is deprecated as of API 21 so surely there are better ways right?).

The new way

Querying usage stats via UsageStatsManager (API 22+) is much more granular and you are able to request exactly what periods of time you are interested in. In our case we are only interested in a delta between a short period of time ago until now. I have found the best delta is around 5-10s to cover any asynchronous operations occuring in the service to settle.

1val endTime = System.currentTimeMillis()
2val beginTime = endTime - 10000
3
4val usageEvents = context.usageStatsManager.queryUsageStats(
5 UsageStatsManager.INTERVAL_DAILY,
6 beginTime,
7 endTime).sortedBy { it.lastTimeUsed }
8val foregroundApp = usageEvents.lastOrNull()
9foregroundApp?.let {
10 // handle app
11}

Use of the UsageStatsManager requires user permission granting to android.permission.PACKAGE_USAGE_STATS through the Settings application. An intent can be used to launch the user to allow them to grant this permission relatively easily (Hint: Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))

The Reactive Way!

This code used to have live solely in a Service initialized and registered by the application - but that was until the addition of the Lifecycle-Aware components and LiveData now available in Android Jetpack!

LiveData, is observable by nature so we can use that to our advantage and allow us to notify the observers of changes in the data set (in this case the currently running foreground app).

To do so we simply subclass LiveData, with a type definition of UsageStats:

1class CurrentAppLiveData private constructor(val context: Context) : LiveData<UsageStats>() {

and implement the abstract methods (onActive and onInactive).

Our final result ends up looking something like this:

1@ObsoleteCoroutinesApi
2class CurrentAppLiveData private constructor(
3 val context: Context,
4 val debugging: Boolean
5) : LiveData<UsageStats>(), AnkoLogger {
6
7 private lateinit var tickerChannel : ReceiveChannel<Unit>
8
9 private var lastResult: UsageStats? = null
10
11 private val job = Job()
12 private val uiScope = CoroutineScope(Dispatchers.Main + job)
13
14 private val usm = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
15
16 private fun listenForAppChanges() {
17 val usageEvents = mutableListOf<UsageStats>()
18
19 var tmpEvent: UsageStats? = null
20
21 uiScope.launch(Dispatchers.IO) {
22 for (tick in tickerChannel) {
23 val endTime = System.currentTimeMillis()
24 val beginTime = endTime - 10000
25
26 usageEvents.clear()
27
28 try {
29 usm.queryUsageStats(
30 UsageStatsManager.INTERVAL_DAILY,
31 beginTime,
32 endTime).apply {
33 usageEvents.addAll(this.sortedBy { it.lastTimeUsed })
34 }
35 } catch (ignored: java.lang.Exception) {
36 }
37
38 if (debugging) {
39 val lastTen = usageEvents.filter { endTime - it.lastTimeUsed < 10.toMillisFromSeconds() }
40 info { "Apps used in last 10s:: ${lastTen.joinToString(",") { it.packageName }}" }
41 }
42
43 usageEvents.lastOrNull()?.let {
44 if (lastResult?.packageName != it.packageName) {
45 if (tmpEvent?.packageName != it.packageName) {
46 // ensure that we get two events in a row
47 tmpEvent = it
48 } else {
49 // trigger a real change
50 uiScope.launch { value = it }
51 lastResult = it
52 }
53 }
54 }
55 }
56 }
57 }
58
59 override fun onActive() {
60 super.onActive()
61 if (debugging) {
62 info { "onActive" }
63 }
64 tickerChannel = ticker(1000, 0)
65 listenForAppChanges()
66 }
67
68 override fun onInactive() {
69 super.onInactive()
70 if (debugging) {
71 info { "onInactive" }
72 }
73 tickerChannel.cancel()
74 }
75
76 companion object Factory {
77
78 fun get(context: Context, debugging: Boolean = false): CurrentAppLiveData {
79 return CurrentAppLiveData(context, debugging)
80 }
81 }
82}
83
84private fun Int.toMillisFromSeconds(): Long = TimeUnit.SECONDS.toMillis(this.toLong())

In order to create the observing fashion of our implementation a neat coroutine called a Channel is used with an increment of 1000 ms (or 1s). This allows us to pick up each app transition to the foreground and alert our observers of each change as they requested. We are also preventing against unwanted quick transitions between apps with our "at least 2" events in a row before triggering a change.

The client side of things

So how do I use this neat observing way of getting the foreground app, especially since a Service isn't a LifecycleOwner?

Simple, you use a Lifecycle-backed Service 😜 Had to do it.

You implement it like every other LiveData element and let the lifecycle components clean up your observer during teardown, with one small catch - being that it is a LiveData (LD) using a Channel you will need to inform the LD to cancel the Channel. To do so simply setup a Observer instance and pass it to observe like you would as an anonymous instance.

1private val appListener = CurrentAppLiveData.get(this)
2[...]
3private val usageStatsObserver = Observer<UsageStats> {
4 // each element is a UsageStats
5 Log.d(TAG, "foreground app is ${it.packageName}!")
6}
7[...]
8private fun observe() {
9 // observe triggers onActive which starts the Channel within the LiveData
10 appListener.observe(this, usageStatsObserver)
11}
12[...]
13override fun onDestroy() {
14 super.onDestroy()
15 // Removing the observer triggers onInactive which cancels the Channel
16 appListener.removeObserver(usageStatsObserver)
17}

In this article we've covered two ways of querying the platform for the currently running, foregrounded application on your or your users Android device - one using ActivityManager which is deprecated as of API 21 (Lollipop), and the replacement using UsageStatsManager available on API 22+ (Nougat). The granularity that UsageStatsManager provides is a huge improvement to the platform, not only for this task, but for the other APIs that it offers as well. This LiveData object will become one of many in a subset that I am creating a library for so keep an ear out for more. If you have any thoughts or questions that you’d like to share, then please do reach out!

This was my first blog post so I hope you enjoyed it!