Android:UsageStatsManager 未返回正确的每日结果

我正在尝试从 中查询,目的是返回每天使用的所有应用包以及使用多长时间。UsageStatsUsageStatsManager

代码:

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;
}

我有一个警报,每天在午夜前触发,查询的使用情况统计信息,然后存储返回的数据。起初,一切似乎都运行良好,我得到了包结果和他们的活动时间,但是我添加了一个功能,可以每小时检查结果,这就是我做出奇怪发现的地方。

的结果似乎在不同的时间重置,而不是在午夜,考虑到我用作搜索参数,这是我所期望的。UsageStatsManagerINTERVAL_DAILY

从我保存的数据来看,包的“时间”结果似乎重置为(粗略时间):

  • 凌晨 3 点
  • 中午
  • 下午3时
  • 午夜

我意识到封装时序重置时之间存在相关性,但这是注定要发生的吗?

我已经看到了以下线程,这是我从中获取大量信息的地方:如何使用ExpositsStatsManager?

因此:Android UsageStatsManager产生错误的输出?在评论中提到,从中返回的数据不可信,并且返回随机结果。queryUsageStats

我是否遗漏了一些简单的东西或无法正常运行?UsageStatsManager


答案 1

我也注意到了API 21中的这种行为,UsageStats数据在API 21中没有得到足够长的维护。它从API 22工作正常,如果你签入,你会发现API 21中的条目有限,所以在API 21中使用它是不可靠的。android /data/system/usagestats

对于 API 21+,在根据 API 进行查询时,您将获得一整天的时间。如果要在一天中的几个小时内进行查询,则应使用并按自己的逻辑对其进行迭代。usagestatsINTERVAL_DAILYUsageStatsManagerqueryEvents

我尝试了以下方式...

这是用于捕获每个应用的数据的模式类:

private class AppUsageInfo {
        Drawable appIcon;
        String appName, packageName;
        long timeInForeground;
        int launchCount;

        AppUsageInfo(String pName) {
            this.packageName=pName;
        }
}

List<AppUsageInfo> smallInfoList; //global var

这是方法,它很容易,随波逐流:

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());

}

答案 2

我同意你在评论中提到的不是一个值得信赖的来源所说的话。我已经玩了一段时间,它根据一天中的时间返回不一致的结果。我发现使用s并手动计算必要的信息会更加可信(至少对于每日统计数据而言),因为它们是时间点,并且没有任何奇怪的计算错误,这些错误会根据一天中的时间为相同的输入产生不同的输出。queryUsageStatsUsageStatsManagerUsageEvent

我用@Vishal提出的解决方案想出了自己的解决方案:

/**
 * 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>)

几个观察结果:

  • s 具有的时间戳采用 UTC 格式,这就是我将开始/结束查询时间从默认时区转换为 UTC 的原因,以及为什么我将每个事件的开始时间转换回的原因。这个让我有一段时间了...Event
  • 这考虑了以下边缘情况:应用在一天开始之前处于前台(即用户在午夜之前打开了应用)或在说完之后转到后台(即用户在当天晚上 11:59 之后,用户仍然在前台有一个应用)。免责声明:我还没有真正测试这些边缘情况。
  • 如果用户在午夜后使用应用,我选择使用晚上 11:59:59 作为结束时间。显然,您可以将其更改为午夜的1毫秒,或者只是午夜,具体取决于您选择如何计算。只需删除并替换为您想要的任何内容即可。- 1000
  • 在我的用例中,我需要总前台时间+开始时间,这就是我收集该信息的原因。但是,您可以调整类和代码以捕获所需的任何信息。例如,您可以跟踪结束时间,或者如果需要,可以跟踪一天内启动应用的次数。Stat
  • 我在这里使用Java 8时间库,因为它更容易处理日期。为了在Android中使用它,我使用了ThreeTenABP库。

我希望这有帮助!


推荐