Using UsageStatsManager to check app usage history
Apps like StayFree, App Usage are gaining immense popularity on the Google Play Store, as they help users manage their time efficiently on a mobile device. These apps can know which apps you’re using throughout the day and also the time you spent on each app. This is a common feature which most developers need in their apps, in order to provide better user experience.
- 🔋 A battery management app could check which apps you’ve used for long and display a warning to the user, informing about the battery consumption of that app.
- 🎓 Apps made for schools could check if the students aren’t using any other apps ( like social media apps, YT ) while attending lectures online.
- 😃 Mental health apps provide better usage insights to the users in order to optimize their digital lives.
We’ll learn how to check the app usage history on an Android device using the UsageStatsManager
. This won’t be a hack, but an appropriate solution to access usage history with the prior permission of the user.
🗒️ Contents
Why not use ‘ActivityManager.getRunningTasks’?
Alternative: Use ‘ActivityManager.getRunningAppProcesses’
The Solution: Using `UsageStatsManager.queryEvents`
— Checking the status of the permission and requesting it
📌 Why not use ‘ActivityManager.getRunningTasks’?
As in most accepted answers on SO, you are most likely to use the ActivityManager.getRunningTasks method to determine which application is in the foreground state. For those who are unaware of the foreground and background states of an Android app, here’s a short description,
For our use-case, the foreground state would only mean that the app is not visible to the user ( maybe the home button is pressed; in such a case, the app moves to the Recents page ).
Getting back to the point, your very first observation in the officials docs of ActivityManager.getRunningTasks
will tell that the method is deprecated from Android Lollipop ( API level 21 ). And the reason Android suggests is quite convincing,
As of
Build.VERSION_CODES.LOLLIPOP
, this method is no longer available to third party applications: the introduction of document-centric recents means it can leak person information to the caller.
You’ll also bump into this observation by looking at the high-voted comments below the answers on SO, which suggested the use of ActivityManager.getRunningTasks
.
Accessing the recent activity of a users without consent, could force users to believe that your app is some sort of spyware installed on their device. Also, developers could study app-usage patterns robbed from your device to enhance their own apps, all from your private data. So, the decision of deprecating ActivityManager.getRunningTasks
from Android was a obvious step for the security of user-data.
📌 Alternative: Use ‘ActivityManager.getRunningAppProcesses’
If you’re searching for a replacement of the deprecated ActivityManager.getRunningTasks
you’ll probably find a solution using the ActivityManager.getRunningAppProcesses method. But, due to some reason, the method returns only the package name of the current process i.e. the app which called this method. No other apps, which were used by the user, are returned through the method.
It seems that Android has restricted this method as well, in order to provide enhanced privacy to the user. Moreover, we’re looking for a way which is more official and safe from future deprecation by Android.
📌 The Solution: Using `UsageStatsManager.queryEvents`
The UsageStatsManager class provides access to the device usage history and statistics. Considering our use-case, we’re going to use the specific
UsageStatsManager.queryUsageStats method to check the user’s recent activity or the apps which were used recently, with the consent of the user.
Step 1: 🔐 Requesting the Permissions
The official documentation for the UsageStatsManager
states,
Most methods on this API require the permission
android.permission.PACKAGE_USAGE_STATS
. However, declaring the permission implies intention to use the API and the user of the device still needs to grant permission through the Settings application.
So, its good to add this permission in our AndroidManifest.xml
,
<?xml version="1.0" encoding="utf-8"?> | |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
package="com.shubham0204.ml.ocmsclient"> | |
<uses-permission | |
android:name="android.permission.PACKAGE_USAGE_STATS" | |
tools:ignore="ProtectedPermissions" /> | |
<application> | |
... | |
</application> | |
</manifest> |
Snippet 1: Adding the PACKAGE_USAGE_STATS permission to AndroidManifest.xml
Job Offers
Note, this is a special permission and not a runtime permission like CAMERA
or WRITE_EXTERNAL_STORAGE
which can be requested by the app. This is evident, as this is a sensitive permission, accessing user’s personal data, regarding which the user needs to be aware.
Step 2: 🔑 Checking the status of the permission and requesting it
In the app’s logic, we first need to determine whether this is permission is granted by the user and if not, we need to request it. Unfortunately, we can’t check the status of this permission using ContextCompat.checkSelfPermission as it is a special permission. Special permissions are rather handled by the
AppOpsManager , which is the class used for managing all app-ops and access control.
// The `PACKAGE_USAGE_STATS` permission is a not a runtime permission and hence cannot be | |
// requested directly using `ActivityCompat.requestPermissions`. All special permissions | |
// are handled by `AppOpsManager`. | |
private fun checkUsageStatsPermission() : Boolean { | |
val appOpsManager = getSystemService(AppCompatActivity.APP_OPS_SERVICE) as AppOpsManager | |
// `AppOpsManager.checkOpNoThrow` is deprecated from Android Q | |
val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | |
appOpsManager.unsafeCheckOpNoThrow( | |
"android:get_usage_stats", | |
Process.myUid(), packageName | |
) | |
} | |
else { | |
appOpsManager.checkOpNoThrow( | |
"android:get_usage_stats", | |
Process.myUid(), packageName | |
) | |
} | |
return mode == AppOpsManager.MODE_ALLOWED | |
} |
Snippet 2: Checking the status of the PACKAGE_USAGE_STATS permission using AppOpsManager
In order to request the PACKAGE_USAGE_STATS
permission, we can’t use the ActivityCompat.requestPermissions method or the
ActivityResultContracts.RequestPermission() as this is not a runtime permission, as we discussed earlier. Instead, we need to navigate the user to the Settings page where the user enables this permission for the app,
if ( checkUsageStatsPermission() ) { | |
// Implement further app logic here ... | |
} | |
else { | |
// Navigate the user to the permission settings | |
Intent( Settings.ACTION_USAGE_ACCESS_SETTINGS ).apply { | |
startActivity( this ) | |
} | |
} |
Snippet 3: Requesting the PACKAGE_USAGE_STATS permission after checking the status the permission
Usage Data Settings as observed in a Android 9 device ( Samsung J7 ).
Step 3: 📱Retrieving Usage Events
Once the user has granted the permission, we can now access user’s device history and check which apps were used in the past with the user’s consent. We can now use the UsageStatsManager.queryEvents
method to retrieve the usage events.
var foregroundAppPackageName : String? = null | |
val currentTime = System.currentTimeMillis() | |
// The `queryEvents` method takes in the `beginTime` and `endTime` to retrieve the usage events. | |
// In our case, beginTime = currentTime - 10 minutes ( 1000 * 60 * 10 milliseconds ) | |
// and endTime = currentTime | |
val usageEvents = usageStatsManager.queryEvents( currentTime - (1000*60*10) , currentTime ) | |
val usageEvent = UsageEvents.Event() | |
while ( usageEvents.hasNextEvent() ) { | |
usageEvents.getNextEvent( usageEvent ) | |
Log.e( "APP" , "${usageEvent.packageName} ${usageEvent.timeStamp}" ) | |
} |
Snippet 4: Requesting the usage events using the `usageStatsManager.queryEvents` method.
- As observed in the code, the
queryEvents
method takes in two arguments,beginTime
andendTime
.beginTime
denotes the time ( in the past ) from which the events need to be retrieved, whereasendTime
denotes the time upto we need the events. Note, both these arguments are Unix Time values, which is evident as we’re usingSystem.currentTimeMillis()
in the code snippet above. - The
usageEvents.getNextEvent
will return the events in a chronological order, so you need to sort them.
If you’re targeting devices from Android R, make sure you have a look at Step 5.
Check the output below, each line shows the package name and the timestamp at which the package was accessed by the user.
The output of code snippet 4. The last line in the output above shows the name of the calling package.
Step 4: Filtering user-installed apps ( Optional )
As you may observe, the above output also contains some system applications like com.sec.android.app.launcher
which is the default launcher application on Samsung devices. This is because the user navigates to the home screen to open some other app or to change a device setting. You may wish to filter those system apps, such that only user-installed apps are visible in the output. We can create a method which returns a Map
containing user-installed apps with their package names and labels ( the name of the app, visible to the user ). We can filter the user-installed apps and also get their label ( in case you wish to process it further in your app’s logic )
private fun getNonSystemAppsList() : Map<String,String> { | |
val appInfos = packageManager.getInstalledApplications( PackageManager.GET_META_DATA ) | |
val appInfoMap = HashMap<String,String>() | |
for ( appInfo in appInfos ) { | |
if ( appInfo.flags != ApplicationInfo.FLAG_SYSTEM ) { | |
appInfoMap[ appInfo.packageName ]= packageManager.getApplicationLabel( appInfo ).toString() | |
} | |
} | |
return appInfoMap | |
} |
Snippet 5: Getting a `Map` containing package names and labels of user-installed apps
Step 5: Check if user is unlocked ( from Android R )
If you’ve checked the documentation for the UsageStatsManager.queryEvent
method, you’ll discover,
Note: Starting from
Android R, if the user’s device is not in an unlocked state (as defined by
UserManager#isUserUnlocked()), then
null
will be returned.
So, from Android R we can’t run this method when the device is locked. So, we need to check if the user is unlocked,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | |
val userManager = getSystemService( Context.USER_SERVICE ) as UserManager | |
if ( userManager.isUserUnlocked ) { | |
// Access usage history ... | |
} | |
} |
Snippet 6: Checking if the user is unlocked with UserManager
Also, note that the UserManager.isUserUnlocked method is available for API 24 and above, and hence we’ll secured it in a
if
statement.
This marks the end of our implementation. Connect to the a physical device and the app running!
📍 We’re done!
Hope you loved the blog! For any suggestions & queries, feel free to leave a comment. Keep reading, keep learning and have a nice day ahead!