16

我试图从 查询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;
}

我有一个警报,每天在午夜之前触发并查询的使用情况,然后存储返回的数据。起初一切似乎都工作正常,我得到了包结果和它们的活动时间,但是我添加了一个每小时检查一次结果的功能,在这里我发现了一个奇怪的发现。

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

从我保存包“时间”的数据来看,结果似乎正在重置(大致时间):

  • 凌晨3点
  • 正午
  • 下午 3 点
  • 午夜

我意识到包裹时间重置时间之间存在相关性,但这是否意味着发生?

我已经看过以下线程,我从中获得了很多信息: 如何使用 UsageStatsManager?

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

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

4

4 回答 4

12

我也注意到 API 21 中的这种行为,UsageStats 数据在 API 21 中的维护时间不够长。它在 API 22 中运行良好,如果您签入android /data/system/usagestats,您会发现 API 21 中的条目有限,因此在 API 中使用它并不可靠21.

对于 API 21+,您将在根据APIusagestats查询的同时获得一整天。如果您想在一天中的几个小时内查询,您应该按照自己的逻辑使用和迭代它们。INTERVAL_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());

}
于 2017-07-28T18:45:52.420 回答
7

我同意您在评论中提到的关于queryUsageStats不是可信赖来源的说法。我已经玩UsageStatsManager了一段时间了,它会根据一天中的时间返回不一致的结果。我发现使用UsageEvents 并手动计算必要的信息更值得信赖(至少对于日常统计数据),因为它们是时间点并且没有任何奇怪的计算错误会为相同的输入产生不同的输出取决于一天中的时间。

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

我希望这有帮助!

于 2018-06-01T16:52:43.873 回答
3

我已经解决了同样的问题,并为此向 Google 提出了问题。如果这与您所描述的相符,请查看https://issuetracker.google.com/issues/118564471 。

于 2018-12-17T13:30:36.417 回答
3

我想我发现那里发生了什么。首先我写了下面的代码,

 public String getDaily(String appPackageName, long startTime, long endTime)
 {
    List<UsageStats> usageStatsList = usageStatsManager.queryUsageStats(
                     UsageStatsManager.INTERVAL_DAILY, startTime,endTime);

    String x="";
    for(int i=0; i<usageStatsList.size(); i++) {

        UsageStats stat = usageStatsList.get(i);
        if(stat.getPackageName().equals(appPackageName))
            x=x+i+"-"+stat.getPackageName()+"-"
            +converLongToTimeChar(stat.getTotalTimeInForeground())+"\n";
    }

    return x;
}
public String converLongToTimeChar(long usedTime) {
    String hour="", min="", sec="";

    int h=(int)(usedTime/1000/60/60);
    if (h!=0)
        hour = h+"h ";

    int m=(int)((usedTime/1000/60) % 60);
    if (m!=0)
        min = m+"m ";

    int s=(int)((usedTime/1000) % 60);
    if (s==0 && (h!=0 || m!=0))
        sec="";
    else
        sec = s+"s";

    return hour+min+sec;
}

(今天的日期是 03.08.2017 00:25:14)和我发送的时间(“包名”,02.08.2017 00.00.00,03.08.2017 00.00.00);to 方法,(我用日历发送了这个日期,你可以在谷歌搜索,如何设置这样的日期)我得到了这个输入;

  46-'apppackagename'-9m 31s
  154-'apppackagename'-22m 38s

然后我发送了(“包名”,03.08.2017 00.00.00, 04.08.2017 00.00.00);到方法,我得到了这个输入;

  25-'apppackagename'-22m 38s

我使用了大约 1 分钟以方法发送的应用程序。我再次发送方法输出是:

02:08:2017-03.08.2017

  46-'apppackagename'-9m 31s
  154-'apppackagename'-23m 32s

03:08:2017-04.08.2017

  25-'apppackagename'-23m 32s

如您所见,它们都增加了。在我看到我等到凌晨 3 点之后,我使用了大约 5 分钟的应用程序,我得到了这些输出。

02:08:2017-03.08.2017

  46-'apppackagename'-9m 31s
  154-'apppackagename'-23m 32s

03:08:2017-04.08.2017

  25-'apppackagename'-23m 32s
  50-'apppackagename'-4m 48s

总而言之,您应该控制前一天及其最后的前台运行时间。如果与那一天的第一个前台时间相同。您应该消除该时间并返回其他时间的总和。(即使我不知道那个奇怪的系统。) 新的一天的柜台从凌晨 03:00 开始。

我希望它对你有帮助。

于 2017-08-03T11:07:20.967 回答