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

21

我正在尝试从UsageStatsManager查询UsageStats,以便返回每天使用的所有应用程序包以及使用时间的信息。

代码:

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作为搜索参数。

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

  • 上午3点
  • 中午
  • 下午3点
  • 午夜

我意识到包时序重置的时间之间存在相关性,但这是意味着应该这样吗?

我已经看过以下线程,并且在那里获得了许多信息: 如何使用UsageStatsManager?

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

我是漏掉了简单的东西,还是UsageStatsManager没有正常运作?


1
嗨!你解决了这个问题吗?我也遇到了同样的问题。 - Mr. Thanks a lot
5个回答

13

我也注意到在API 21中有这种行为,UsageStats数据在API 21中无法长时间维护。从API 22开始正常工作,如果您在android/data/system/usagestats中检查,您会发现API 21中只有有限的条目,因此在API 21中不可靠。

对于API 21+, 根据UsageStatsManager API,如果查询INTERVAL_DAILY,您将获得整个一天的usagestats数据。 如果想要查询一天中的某些小时,您应该使用queryEvents并通过自己的逻辑进行迭代。

我是这样尝试的...

这是用于捕获每个应用程序数据的模态类:

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

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

List<AppUsageInfo> smallInfoList; //全局变量

这里是一个方法,很简单,跟着步骤走:

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

}

1
我们需要发送UTC时间(timeInMillis)作为开始和结束时间吗? - Nainal

9

我同意你提到的有关queryUsageStats不是可信来源的评论。我已经使用UsageStatsManager玩了一段时间,发现它根据一天中的时间返回不一致的结果。我发现使用UsageEvent并手动计算所需信息更加可靠(至少对于每日统计),因为它们是时间点,并且没有任何奇怪的计算错误,这将根据一天中的时间产生不同的输出。

我使用@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>)

一些观察结果:
  • Event的时间戳是使用协调世界时(UTC)表示的,这就是为什么我将我的开始/结束查询时间从我的默认时区转换为UTC,并在每个事件上将开始时间转换回来的原因。这一点让我困扰了一段时间...
  • 这考虑到了应用程序在当天开始之前(即用户在午夜之前打开应用程序)或在当天结束后进入后台的边缘情况(即用户在那一天晚上11:59 PM之后仍然有一个应用程序在前台)。免责声明:我还没有测试这些边缘情况。
  • 在用户在午夜之后使用应用程序的情况下,我选择使用11:59:59 PM作为结束时间。您可以根据自己的计算方式将其更改为午夜之前1毫秒,或者直接使用午夜。只需删除- 1000并替换为您想要的任何内容即可。
  • 在我的用例中,我需要总前台时间+开始时间,这就是为什么我收集该信息的原因。但是,您可以调整Stat类和代码以捕获所需的任何信息。例如,您可以跟踪结束时间或一天内启动应用程序的次数。
  • 我在此处使用Java 8时间库,因为处理日期更容易。要在Android中使用它,我使用ThreeTenABP库。
希望这有所帮助!

1
如果应用程序崩溃,MOVE_TO_FOREGROUND事件将被ACTIVITY_STOPPED事件跟随。这也值得测试,否则应用程序将被视为长时间运行 :) - Florian
@Florian 和 @jguerinet:你们上面添加的注释是正确的。但如何在上述代码中添加 ACTIVITY_STOPPED 的条件呢?请指导。 - SVK

4

我猜我发现了那里发生的事情。首先,我写了下面的代码:

 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)时,我得到了以下输入;(我使用日历发送了这些日期,你可以在谷歌上搜索如何设置这样的日期)

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

我向该方法发送了 ("package name", 2017年03月08日 00点00分00秒, 2017年04月08日 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

总之,你应该在日期发生变化之前和它的最后前台运行时间进行控制。如果它与当天的第一次前台时间相同,则应该消除该时间并返回其余时间的总和。(即使我不知道那个奇怪的系统)新的一天从凌晨三点开始计时。

希望对您有所帮助。


4
谁制作了这个 API 纯粹是恶意的。感谢您分享这个。 - Lorenzo Von Matterhorn
@IlToro,很高兴你认为这是有益的。不过我强烈建议使用UsageStats.Events来计算使用时间。它提供了许多使用情况统计细节,如[时间] [包名] [前台|后台]。 - Burak
你是对的。起初从凌晨3点到凌晨3点读取UsageStats似乎没问题。我甚至写了一个循环来查找可能获取的克隆包中最新的包(如果你错误地查询了多个日期)。然而,经过几天的测试后,情况又开始变得混乱了。所以我认为这个API根本不可靠。 - Lorenzo Von Matterhorn

3

0

我编写了一个更干净、更安全的queryEvents函数,受到@Vishal Sharma, @jguerinet@Floarian答案的启发。

从Android Q开始,前台服务事件也可以记录。特别是如果您想计算在后台花费的时间,现在可以做到。

创建一个AppUsageStats类:

public class AppUsageStats {
    private final long lastTimeUsedMillis;
    private final long totalTimeInForegroundMillis;
    private final long lastTimeForegroundServiceUsedMillis;
    private final long totalTimeForegroundServiceUsedMillis;

    public AppUsageStats(
            long lastTimeUsedMillis,
            long totalTimeInForegroundMillis,
            long lastTimeForegroundServiceUsedMillis,
            long totalTimeForegroundServiceUsedMillis
    ) {
        this.lastTimeUsedMillis = lastTimeUsedMillis;
        this.totalTimeInForegroundMillis = totalTimeInForegroundMillis;
        this.lastTimeForegroundServiceUsedMillis = lastTimeForegroundServiceUsedMillis;
        this.totalTimeForegroundServiceUsedMillis = totalTimeForegroundServiceUsedMillis;
    }

    public long getLastTimeUsedMillis() {
        return lastTimeUsedMillis;
    }

    public long getTotalTimeInForegroundMillis() {
        return totalTimeInForegroundMillis;
    }

    @RequiresApi(Build.VERSION_CODES.Q)
    public long getLastTimeForegroundServiceUsedMillis() {
        return lastTimeForegroundServiceUsedMillis;
    }

    @RequiresApi(Build.VERSION_CODES.Q)
    public long getTotalTimeForegroundServiceUsedMillis() {
        return totalTimeForegroundServiceUsedMillis;
    }
}

创建一个AppUsageStatsBucket类来存储前台服务数据:
public class AppUsageStatsBucket {
    private long startMillis;
    private long endMillis;
    private long totalTime;

    public AppUsageStatsBucket() {
        this.startMillis = 0L;
        this.endMillis = 0L;
        this.totalTime = 0L;
    }

    public long getStartMillis() {
        return startMillis;
    }

    public void setStartMillis(long startMillis) {
        this.startMillis = startMillis;
    }

    public long getEndMillis() {
        return endMillis;
    }

    public void setEndMillis(long endMillis) {
        this.endMillis = endMillis;
    }

    public long getTotalTime() {
        return totalTime;
    }

    public void addTotalTime() {
        this.totalTime += endMillis - startMillis;
    }

    public void setTotalTime(long totalTime) {
        this.totalTime = totalTime;
    }
}

创建一个UsageStatsSelection枚举类:
public enum UsageStatsSelection {
    HOURLY(
            UsageStatsManager.INTERVAL_DAILY,
            System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1L),
            System.currentTimeMillis()
    ),
    DAILY(
            UsageStatsManager.INTERVAL_DAILY,
            0L,
            System.currentTimeMillis()
    ),
    WEEKLY(
            UsageStatsManager.INTERVAL_WEEKLY,
            0L,
            System.currentTimeMillis()
    ),
    MONTHLY(
            UsageStatsManager.INTERVAL_MONTHLY,
            0L,
            System.currentTimeMillis()
    ),
    YEARLY(
            UsageStatsManager.INTERVAL_YEARLY,
            0L,
            System.currentTimeMillis()
    );

    private final int usageStatsInterval;
    private final long beginTime;
    private final long endTime;

    UsageStatsSelection(int usageStatsInterval, long beginTime, long endTime) {
        this.usageStatsInterval = usageStatsInterval;
        this.beginTime = beginTime;
        this.endTime = endTime;
    }

    public int getUsageStatsInterval() {
        return usageStatsInterval;
    }

    public long getBeginTime() {
        return beginTime;
    }

    public long getEndTime() {
        return endTime;
    }
}

由于无法获取queryEvents的所有日期(请参见:queryEvents(), 查询给定时间范围内的事件。系统仅保留几天的事件),因此我们将使用queryEvents仅获取每日事件。我们还将使用queryUsageStats()函数获取每周、每月和每年的使用数据。

@NonNull
public static Map<String, AppUsageStats> queryUsageStats(
        Context context,
        @NonNull UsageStatsSelection statsSelection
) {
    final UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
    final Map<String, AppUsageStats> appUsageStatsHashMap = new HashMap<>();
    switch (statsSelection) {
        case HOURLY:
            final UsageEvents events = usageStatsManager.queryEvents(
                    statsSelection.getBeginTime(),
                    statsSelection.getEndTime()
            );
            Map<String, List<UsageEvents.Event>> eventsMap = new HashMap<>();
            UsageEvents.Event currentEvent;
            while (events.hasNextEvent()) {
                currentEvent = new UsageEvents.Event();
                if (events.getNextEvent(currentEvent)) {
                    switch (currentEvent.getEventType()) {
                        case UsageEvents.Event.ACTIVITY_RESUMED:
                        case UsageEvents.Event.ACTIVITY_PAUSED:
                        case UsageEvents.Event.ACTIVITY_STOPPED:
                        case UsageEvents.Event.FOREGROUND_SERVICE_START:
                        case UsageEvents.Event.FOREGROUND_SERVICE_STOP:
                            List<UsageEvents.Event> packageEvents = eventsMap.get(currentEvent.getPackageName());
                            if (packageEvents == null) {
                                packageEvents = new ArrayList<>(Collections.singletonList(currentEvent));
                            } else {
                                packageEvents.add(currentEvent);
                            }
                            eventsMap.put(currentEvent.getPackageName(), packageEvents);
                            break;
                    }
                }
            }

            for (Map.Entry<String, List<UsageEvents.Event>> entry : eventsMap.entrySet()) {
                final AppUsageStatsBucket foregroundBucket = new AppUsageStatsBucket();
                final Map<String, AppUsageStatsBucket> backgroundBucketMap = new HashMap<>();
                for (int pos = 0; pos < entry.getValue().size(); pos++) {
                    final UsageEvents.Event event = entry.getValue().get(pos);
                    AppUsageStatsBucket backgroundBucket = backgroundBucketMap.get(event.getClassName());
                    if (backgroundBucket == null) {
                        backgroundBucket = new AppUsageStatsBucket();
                        backgroundBucketMap.put(event.getClassName(), backgroundBucket);
                    }
                    switch (event.getEventType()) {
                        case UsageEvents.Event.ACTIVITY_RESUMED:
                            foregroundBucket.setStartMillis(event.getTimeStamp());
                            break;
                        case UsageEvents.Event.ACTIVITY_PAUSED:
                        case UsageEvents.Event.ACTIVITY_STOPPED:
                            if (foregroundBucket.getStartMillis() >= foregroundBucket.getEndMillis()) {
                                if (foregroundBucket.getStartMillis() == 0L) {
                                    foregroundBucket.setStartMillis(statsSelection.getBeginTime());
                                }
                                foregroundBucket.setEndMillis(event.getTimeStamp());
                                foregroundBucket.addTotalTime();
                            }
                            break;
                        case UsageEvents.Event.FOREGROUND_SERVICE_START:
                            backgroundBucket.setStartMillis(event.getTimeStamp());
                            break;
                        case UsageEvents.Event.FOREGROUND_SERVICE_STOP:
                            if (backgroundBucket.getStartMillis() >= backgroundBucket.getEndMillis()) {
                                if (backgroundBucket.getStartMillis() == 0L) {
                                    backgroundBucket.setStartMillis(statsSelection.getBeginTime());
                                }
                                backgroundBucket.setEndMillis(event.getTimeStamp());
                                backgroundBucket.addTotalTime();
                            }
                            break;
                    }
                    if (pos == entry.getValue().size() - 1) {
                        if (foregroundBucket.getStartMillis() > foregroundBucket.getEndMillis()) {
                            foregroundBucket.setEndMillis(statsSelection.getEndTime());
                            foregroundBucket.addTotalTime();
                        }
                        if (backgroundBucket.getStartMillis() > backgroundBucket.getEndMillis()) {
                            backgroundBucket.setEndMillis(statsSelection.getEndTime());
                            backgroundBucket.addTotalTime();
                        }
                    }
                }

                final long foregroundEnd = foregroundBucket.getEndMillis();
                final long totalTimeForeground = foregroundBucket.getTotalTime();
                final long backgroundEnd = backgroundBucketMap.values()
                        .stream()
                        .mapToLong(AppUsageStatsBucket::getEndMillis)
                        .max()
                        .orElse(0L);
                final long totalTimeBackground = backgroundBucketMap.values()
                        .stream()
                        .mapToLong(AppUsageStatsBucket::getTotalTime)
                        .sum();

                appUsageStatsHashMap.put(entry.getKey(), new AppUsageStats(
                        Math.max(foregroundEnd, backgroundEnd),
                        totalTimeForeground,
                        backgroundEnd,
                        totalTimeBackground
                ));
            }
            break;
        default:
            final List<UsageStats> usageStats = usageStatsManager
                .queryUsageStats(
                        statsSelection.getUsageStatsInterval(),
                        statsSelection.getBeginTime(),
                        statsSelection.getEndTime()
                );
            appUsageStatsHashMap.putAll(usageStats.parallelStream()
                    .collect(Collectors.toMap(
                            UsageStats::getPackageName,
                            stats -> new AppUsageStats(
                                    stats.getLastTimeUsed(),
                                    stats.getTotalTimeInForeground(),
                                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
                                            ? stats.getLastTimeForegroundServiceUsed()
                                            : 0,
                                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
                                            ? stats.getTotalTimeForegroundServiceUsed()
                                            : 0
                            ),
                            (oldValue, newValue) -> newValue
                    )));
            break;
    }
    return appUsageStatsHashMap;
}

结果将以包名和AppUsageStats列表的形式返回。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接