I'm attempting to query UsageStats
from UsageStatsManager
, with the aim of returning all app packages that were used daily and for how long.
The Code:
public static List<UsageStats> getUsageStatsList(Context context){
UsageStatsManager usm = getUsageStatsManager(context);
Calendar calendar = Calendar.getInstance();
long endTime = calendar.getTimeInMillis();
calendar.add(Calendar.DAY_OF_YEAR, -1);
long startTime = calendar.getTimeInMillis();
List<UsageStats> usageStatsList = usm.queryUsageStats(UsageStatsManager.INTERVAL_DAILY,startTime, endTime);
return usageStatsList;
}
I have an alarm that fires daily just before midnight and query's usagestats and then stores the returned data. At first everything seemed to be working fine and I was getting package results and their active time, however I added a function that would check the results hourly and here is where I made a strange discovery.
The results from UsageStatsManager
seemed to be resetting at different times, instead of at midnight, which is what I would have expected considering I was using INTERVAL_DAILY
as a search parameter.
From the data I saved the package 'time' results seem to be resetting at (Rough timings):
I realize that there is a correlation between when the package timings reset but is this meant to happen?
I've already seen the following thread and it's where I got a lot of my information from: How to use UsageStatsManager?
Consequently:
Android UsageStatsManager producing wrong output?
In the comments mentions that the data returned from queryUsageStats
can't be trusted and random results are being returned.
Am I missing something simple or is UsageStatsManager
not functioning correctly?
I too noticed this behaviour in API 21,UsageStats data is not maintained for sufficiently long in API 21. it works fine from API 22, If you check in android /data/system/usagestats
, you will find limited entries in API 21, so its not reliable using it in API 21.
For API 21+,
You will get the whole day usagestats
while querying INTERVAL_DAILY
according to UsageStatsManager
API.
If you want to query within hours of day you should use queryEvents
and iterating them by your own logic.
I tried in the following way...
This is the modal class for capturing data for every app:
private class AppUsageInfo {
Drawable appIcon;
String appName, packageName;
long timeInForeground;
int launchCount;
AppUsageInfo(String pName) {
this.packageName=pName;
}
}
List<AppUsageInfo> smallInfoList; //global var
here is the method, its easy, go with the flow:
void getUsageStatistics() {
UsageEvents.Event currentEvent;
List<UsageEvents.Event> allEvents = new ArrayList<>();
HashMap<String, AppUsageInfo> map = new HashMap <String, AppUsageInfo> ();
long currTime = System.currentTimeMillis();
long startTime currTime - 1000*3600*3; //querying past three hours
UsageStatsManager mUsageStatsManager = (UsageStatsManager)
mContext.getSystemService(Context.USAGE_STATS_SERVICE);
assert mUsageStatsManager != null;
UsageEvents usageEvents = mUsageStatsManager.queryEvents(usageQueryTodayBeginTime, currTime);
//capturing all events in a array to compare with next element
while (usageEvents.hasNextEvent()) {
currentEvent = new UsageEvents.Event();
usageEvents.getNextEvent(currentEvent);
if (currentEvent.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND ||
currentEvent.getEventType() == UsageEvents.Event.MOVE_TO_BACKGROUND) {
allEvents.add(currentEvent);
String key = currentEvent.getPackageName();
// taking it into a collection to access by package name
if (map.get(key)==null)
map.put(key,new AppUsageInfo(key));
}
}
//iterating through the arraylist
for (int i=0;i<allEvents.size()-1;i++){
UsageEvents.Event E0=allEvents.get(i);
UsageEvents.Event E1=allEvents.get(i+1);
//for launchCount of apps in time range
if (!E0.getPackageName().equals(E1.getPackageName()) && E1.getEventType()==1){
// if true, E1 (launch event of an app) app launched
map.get(E1.getPackageName()).launchCount++;
}
//for UsageTime of apps in time range
if (E0.getEventType()==1 && E1.getEventType()==2
&& E0.getClassName().equals(E1.getClassName())){
long diff = E1.getTimeStamp()-E0.getTimeStamp();
phoneUsageToday+=diff; //gloabl Long var for total usagetime in the timerange
map.get(E0.getPackageName()).timeInForeground+= diff;
}
}
//transferred final data into modal class object
smallInfoList = new ArrayList<>(map.values());
}
I agree with what is said in that comment you mentioned about queryUsageStats
not being a trusted source. I've been with playing with the UsageStatsManager
for a little while and it returns inconsistent results based on the time of day. I have found that using the UsageEvent
s and manually calculating the necessary info to be much more trustworthy (at least for daily stats), as they are points in time and don't have any weird calculating errors that would produce different outputs for the same input depending on the time of day.
I used @Vishal's proposed solution to come up with my own:
/**
* Returns the stats for the [date] (defaults to today)
*/
fun getDailyStats(date: LocalDate = LocalDate.now()): List<Stat> {
// The timezones we'll need
val utc = ZoneId.of("UTC")
val defaultZone = ZoneId.systemDefault()
// Set the starting and ending times to be midnight in UTC time
val startDate = date.atStartOfDay(defaultZone).withZoneSameInstant(utc)
val start = startDate.toInstant().toEpochMilli()
val end = startDate.plusDays(1).toInstant().toEpochMilli()
// This will keep a map of all of the events per package name
val sortedEvents = mutableMapOf<String, MutableList<UsageEvents.Event>>()
// Query the list of events that has happened within that time frame
val systemEvents = usageManager.queryEvents(start, end)
while (systemEvents.hasNextEvent()) {
val event = UsageEvents.Event()
systemEvents.getNextEvent(event)
// Get the list of events for the package name, create one if it doesn't exist
val packageEvents = sortedEvents[event.packageName] ?: mutableListOf()
packageEvents.add(event)
sortedEvents[event.packageName] = packageEvents
}
// This will keep a list of our final stats
val stats = mutableListOf<Stat>()
// Go through the events by package name
sortedEvents.forEach { packageName, events ->
// Keep track of the current start and end times
var startTime = 0L
var endTime = 0L
// Keep track of the total usage time for this app
var totalTime = 0L
// Keep track of the start times for this app
val startTimes = mutableListOf<ZonedDateTime>()
events.forEach {
if (it.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
// App was moved to the foreground: set the start time
startTime = it.timeStamp
// Add the start time within this timezone to the list
startTimes.add(Instant.ofEpochMilli(startTime).atZone(utc)
.withZoneSameInstant(defaultZone))
} else if (it.eventType == UsageEvents.Event.MOVE_TO_BACKGROUND) {
// App was moved to background: set the end time
endTime = it.timeStamp
}
// If there's an end time with no start time, this might mean that
// The app was started on the previous day, so take midnight
// As the start time
if (startTime == 0L && endTime != 0L) {
startTime = start
}
// If both start and end are defined, we have a session
if (startTime != 0L && endTime != 0L) {
// Add the session time to the total time
totalTime += endTime - startTime
// Reset the start/end times to 0
startTime = 0L
endTime = 0L
}
}
// If there is a start time without an end time, this might mean that
// the app was used past midnight, so take (midnight - 1 second)
// as the end time
if (startTime != 0L && endTime == 0L) {
totalTime += end - 1000 - startTime
}
stats.add(Stat(packageName, totalTime, startTimes))
}
return stats
}
// Helper class to keep track of all of the stats
class Stat(val packageName: String, val totalTime: Long, val startTimes: List<ZonedDateTime>)
A couple of observations:
Event
s have are in UTC, which is why I convert my start/end query times to UTC from my default time zone, and why I convert the start time back on each event. This one got me for a while...- 1000
and replace with whatever you want. Stat
class and the code to capture whatever info you need. You can keep track of end times, or number of times an app was launched in a day if needed for example. I hope this helps!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With