关于 UsageStatsManager.queryUsageStats 的注意事项及 UsageStatsService 的简单原理

问题背景:

目前需求统计应用的当天使用情况,在 5.0 以上有权限 android.permission.PACKAGE_USAGE_STATS,获取到该权限后可以通过 UsageStatsManager.queryUsageStats(int intervalType, long beginTime, long endTime) 方法查询到应用的使用情况。

问题描述:

第一天下午使用一些应用后,可以查询到相应应用的当天使用记录。第二天早上七点打开手机,在确定未使用这些应用的情况下,还是查询到了这些应用的当天使用记录。因为查询的是当天使用记录,所以在过去了一个自然日后,应该查询不到应用的使用记录才对。

问题分析:

queryUsageStats 有三个参数,分别是 intervalTypebeginTimeendTime。先说说 beginTimeendTime,它们分别表示待查询时间范围的起始时间和结束时间。而 intervalType 表示时间间隔的类型,它有5个值:

  • INTERVAL_DAILY:天存储级别的数据;
  • INTERVAL_WEEKLY:星期存储级别的数据;
  • INTERVAL_MONTHLY:月存储级别的数据;
  • INTERVAL_YEARLY:年存储级别的数据;
  • INTERVAL_BEST:根据给定时间范围选取最佳时间间隔类型。

既然有了待查询时间范围,那么时间类型又有什么用呢,这就是导致上述问题发生的原因了。测试中查询的时间范围默认是当天零点到当前时间,而时间类型用的是 INTERVAL_DAILY。在实际查询结果中发现查询到的使用记录并不是按给定时间范围来的,而是从前一天的某个时间点开始的。

我们先看下 queryUsageStats 返回的数据,它是一个 UsageStats 类型的数据集合,其中有几个关键字段:

  • mBeginTimeStamp:查询范围的起始时间;
  • mEndTimeStamp:查询范围的起结束时间;
  • mLastTimeUsed:应用最后一次使用结束时的时间;
  • mTotalTimeInForeground:查询范围内应用在前台的累积时长;
  • mLaunchCount:查询范围内应用的打开次数。

进一步分析查询结果,发现其中的 mBeginTimeStampmEndTimeStamp 并不是传入的时间范围,而是和 intervalType 有关,比如在 INTERVAL_DAILY 情况下进行如下测试:

查询时间范围 2018-10-31 00:00:002018-10-31 18:00:00,查询结果中 mBeginTimeStampmEndTimeStamp 分别为 2018-10-30 08:00:002018-10-31 07:59:59

查询时间范围 2018-10-30 10:00:002018-10-31 18:00:00,查询结果中 mBeginTimeStampmEndTimeStamp 分别为 2018-10-30 08:00:002018-10-31 07:59:59

查询时间范围 2018-10-31 10:00:002018-10-31 18:00:00,查询结果中 mBeginTimeStampmEndTimeStamp 分别为 2018-10-31 08:00:002018-10-31 19:17:32(当前系统时间)。

从以上3次测试结果可以看出,beginTimeendTimemBeginTimeStampmEndTimeStamp 没有显而易见的关系。我们再看一下 queryUsageStats 参数的注释:
在这里插入图片描述
通过注释可以理解 beginTime 会包含在查询结果的时间范围内,即:

    beginTime >= mBeginTimeStamp && beginTime <= mEndTimeStamp

综上可以得出结论,queryUsageStats 的查询范围依赖 intervalType 而定,比如 INTERVAL_DAILY 类型会查询一天内的使用情况,并且根据传入的 beginTime 计算出该天的起始和结束时间。这里的天并非自然日的概念,而是受时区影响的一天,即北京时间(东八区)从 08:00:00 开始算一天,但在少数机型上也发现从 00:00:00 开始算一天。不过可以肯定的是 mBeginTimeStamp 会受到时区的影响,比如当前是东八区,修改到东十区,一天的起始时间会相应增加2小时。

解决方案:

因为是系统的 api,查询的时间段无法调整到更精确的范围,只能考虑增加对 mLastTimeUsed 的判断,这个字段是应用最后一次使用结束时的时间,可以判断 mLastTimeUsed 是不是在 beginTimeendTime 之间来确定用户当天是否有使用过应用。

但是如果想判断用户当天是否有使用应用达到一定时长,就不能很准确的判断了。因为 mTotalTimeInForeground 统计的是 mBeginTimeStampmEndTimeStamp 期间应用在前台的累计时长,所以即使结合 mLastTimeUsed 判断出用户当天有打开过应用,但还是无法准确得知当天打开应用后的使用时长。

拓展阅读:

在测试期间还发现一个问题:先使用某个应用一定时间,查询出当天的使用记录,然后修改手机系统时间到一星期后,查询发现当天仍有使用记录,且两次查询结果除日期变化外其余都相同。猜测使用记录是不是以一种相对时间的方式存储在系统中的,当查询时会根据当前时间计算出具体时间戳,所以进一步研究验证。

这里就不得不提一下 UsageStatsService 了,UsageStatsManager.queryUsageStats 实际上是通过 UsageStatsService.queryUsageStats 进行查询的。UsageStatsService 是一个系统服务,其主要通过 AMS 等来收集、聚合和保留应用程序使用数据,可以通过 AppOps 授予应用程序权限来查询此数据。

UsageStatsService 记录的应用行为事件全部定义在 UsageEvents 类中,在 UsageStats 中也有字段 mLastEvent 记录了应用的最后一次行为。这里说一下几个常见的事件:

  • MOVE_TO_FOREGROUND:当 Activity onResume 时,ActivityManagerService 会调用UsageStatsService.noteResumeComponent 方法记录下此事件,标记应用切换到前台。

  • MOVE_TO_BACKGROUND:当 Activity onPause 时,ActivityManagerService 会调用UsageStatsService.notePauseComponent 方法记录下此事件,标记应用切换到后台。

UsageStatsService 在接收到事件变化时会通过 writeStatsToFile 方法将数据持久化成文件,通过这里可以得知 UsageStatsService 是通过文件来存储应用的使用数据的。

那么文件又是如何被管理的呢?这里需要引出 UsageStatsDatabase,通过它将数据持久化存储在了 XML 文件中,并提供从 XML 数据库查询 UsageStats 数据的接口,XML 文件存储在 /data/system/usagestats/ 路径下。数据目录按 daily、monthly、weekly、yearly 四个文件夹存储。XML 的所有操作,例如读、写等,都被封装在 UsageStatsXmlV1 中,由 UsageStatsDatabase 进行调用。
在这里插入图片描述
关于 UsageStatsService 的数据存储,还有很重要的一点需要提到,那就是缓存。UsageStatsService 在每次启动时,都会先按照 user 生成各个 UserUsageStatsService,其中每个对象都会先去各自的文件路径下读取数据到内存中。此后每次外界 reportEvent 时,都会先更新内存中的数据,相当于缓存。那么什么时候会把内存中的数据更新到文件中呢?主要有以下几种情况:

  • 手机关机
  • 系统时间跳变
  • 一天结束时

从这里引出了我们一开始提到的问题,在修改系统时间后,使用记录为什么会跟随日期变化。

首先,手机系统时间会发生跳变,常见形式有人为修改时间或系统通过网络自动校准。比如,手机第一次使用,未联网校准时,手机时间是错误的,可能显示为1970年1月1日,这时候产生的应用使用数据会被记录为1970年1月1日。但手机联网后,时间通过网络校准为2018年11月1日。那么 UsageStatsService 中统计的时间会仍然为1970年1月1日吗?Google 早在设计之初就考虑到了这点,在 UsageStatsService 中有一个巧妙的机制来保证记录时间的准确性。

UsageStatsService 中有一个 checkAndGetTimeLocked 方法,此方法会在每次 reportEventqueryUsageStats 时会去检查系统时间。
在这里插入图片描述
通过代码可以看出,checkAndGetTimeLocked 会计算出当前系统时间与预期系统时间之间的差值,当这个差值大于 TIME_CHANGE_THRESHOLD_MILLIS(2s)时,UsageStatsService 会调用 onTimeChanged 方法,它会负责更新 UsageStatsService 记录的时间,以便它们能够跟随系统时间跳变而相应更新。这就是直接导致开始提到的修改手机系统时间后,查询发现当天仍有使用记录,且前后两次查询结果除日期变化外其余都相同的原因。

既然使用记录的时间会随系统时间修改而变动,那么它是否如猜测一般是以一种相对时间的形式存储的呢?首先,我们来看下 daily 文件夹:
在这里插入图片描述
从上图中可以看出 XML 文件是以当天日期起点的时间戳命名的。如 1541030400000 即 2018-11-01 08:00:00。其次,在 daily 文件夹下,XML 文件是以天级别存储的,每天会新建一个文件。这里也可以联想到之前分析的 UsageStatsManager.queryUsageStats 不会根据传入的时间范围精确查询,而是以天级别查询的,其实就是读取分析了当天的 XML 文件。且文件命名中的时间是受时区影响的,也就导致了 daily 级别的查询结果不是按自然日来的。接下来看一下 XML 文件的具体内容:
在这里插入图片描述
从文件中可以看出,使用记录是以毫秒级别的差值进行存储的,这个差值是以文件名中的时间点为锚定的。在查询使用记录时会结合两者计算出确切的时间,比如,应用的上次使用时间 = XML 文件名 + XML 中此应用的上次使用时间。

通过以上机制,当系统时间跳变时,UsageStatsService 会通过 onTimeChanged 方法更新 XML 文件名中的时间,而 XML 中的数据并不需要变动即可随着系统时间的跳变而保持准确值。


2018/12/28 更新

如何解决 UsageStatsManager.queryUsageStats 统计不准确问题

在研究统计应用前台活动时,发现 UsageStatsManager.queryEvents 方法查询的时间范围是准确的。其返回结果是 UsageEvents 类型,其中包含了该时间段内所有应用的所有活动事件,UsageEvents.Event 包含了单个事件的具体内容,通过 while (UsageEvents.getNextEvent(UsageEvents.Event)) 方法可以遍历获取所有事件。

UsageEvents.Event 中主要提供了以下几个 API :

  • getPackageName():该事件来源应用的包名。

  • getTimeStamp():该事件发生的时间,毫秒级别时间戳。

  • getEventType():该事件类型。

其中,事件类型分为以下几种:

        /**
         * No event type.
         */
        public static final int NONE = 0;

        /**
         * An event type denoting that a component moved to the foreground.
         */
        public static final int MOVE_TO_FOREGROUND = 1;

        /**
         * An event type denoting that a component moved to the background.
         */
        public static final int MOVE_TO_BACKGROUND = 2;

        /**
         * An event type denoting that a component was in the foreground when the stats
         * rolled-over. This is effectively treated as a {@link #MOVE_TO_BACKGROUND}.
         * {@hide}
         */
        public static final int END_OF_DAY = 3;

        /**
         * An event type denoting that a component was in the foreground the previous day.
         * This is effectively treated as a {@link #MOVE_TO_FOREGROUND}.
         * {@hide}
         */
        public static final int CONTINUE_PREVIOUS_DAY = 4;

        /**
         * An event type denoting that the device configuration has changed.
         */
        public static final int CONFIGURATION_CHANGE = 5;

        /**
         * An event type denoting that a package was interacted with in some way by the system.
         * @hide
         */
        public static final int SYSTEM_INTERACTION = 6;

        /**
         * An event type denoting that a package was interacted with in some way by the user.
         */
        public static final int USER_INTERACTION = 7;

        /**
         * An event type denoting that an action equivalent to a ShortcutInfo is taken by the user.
         *
         * @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
         */
        public static final int SHORTCUT_INVOCATION = 8;

通过记录每个应用 MOVE_TO_FOREGROUNDMOVE_TO_BACKGROUND 的时间点,可以计算出其在前台停留的时间。该方法可以弥补 UsageStatsManager.queryUsageStats 查询不准确的问题,但处理过程比较麻烦,相当于自己对 mTotalTimeInForeground 进行统计运算。

  • 16
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 29
    评论
### 回答1: 这是一个 Android 平台的 API,用于查询应用程序的使用情况统计数据,返回一个 UsageStats 对象列表。该方法需要传入开始时间和结束时间,以确定查询的时间范围。例如,以下代码可以查询过去一小时内应用程序的使用情况统计数据: ```java UsageStatsManager usm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); List<UsageStats> usageStatsList = usm.queryUsageStats(UsageStatsManager.INTERVAL_HOUR, System.currentTimeMillis() - 3600000, System.currentTimeMillis()); ``` 需要注意的是,在 Android 5.0 及以上版本中,使用该 API 需要获取到 `android.permission.PACKAGE_USAGE_STATS` 权限。 ### 回答2: UsageStatsManager是Android系统中的一个类,它提供了一套用于查询设备上各个应用程序的使用统计信息的API。 queryUsageStats()方法是UsageStatsManager类中的一个方法,用于从指定时间段内获取应用程序的使用情况统计信息。 使用queryUsageStats()方法,我们可以获得包含了应用程序使用时间、最后一次使用时间和应用程序名称等信息的UsageStats对象列表。 首先,我们需要通过Context.getSystemService()方法来获取一个UsageStatsManager的实例,在参数中传入Context.USAGE_STATS_SERVICE。 然后,我们可以使用queryUsageStats()方法来查询特定时间段内的应用程序使用统计信息。该方法需要两个参数,分别是开始时间和结束时间,以毫秒为单位。 查询结果将返回一个List<UsageStats>对象,其中每个UsageStats对象表示一个应用程序的使用统计信息,可以通过UsageStats类提供的方法来获取具体的信息。 使用queryUsageStats()方法可以帮助我们实现一些功能,比如统计用户使用各个应用程序的时长,或者判断某个应用程序是否在指定时间段内被使用过。 需要注意的是,使用queryUsageStats()方法需要我们的应用程序具有GET_USAGE_STATS权限,否则会抛出SecurityException异常。我们需要在AndroidManifest.xml文件中声明该权限。 总结来说,UsageStatsManagerqueryUsageStats()方法提供了一种获取应用程序使用统计信息的方式,可以帮助我们了解用户对应用程序的使用情况,为用户提供更好的体验。 ### 回答3: UsageStatsManager queryUsageStats() 是一个方法,用来查询应用程序的使用统计信息。 在Android系统中,应用程序的使用统计信息可以包括应用程序的启动时间、关闭时间、使用总时长等。通过使用UsageStatsManagerqueryUsageStats()方法,我们可以获取这些统计信息。 使用该方法,我们需要先通过Context.getSystemService()方法获取到系统的UsageStatsManager对象。然后,我们可以调用queryUsageStats()方法,传入起始时间和结束时间参数,来获取在指定时间段内的应用程序使用统计信息。 返回的结果是一个List<UsageStats>对象,其中包含了每个应用程序的使用统计信息。我们可以遍历该列表,获取各个应用程序的包名、使用时长、最后使用时间等信息。 这个方法的应用场景很多。例如,我们可以利用该方法来编写一个应用程序的使用情况统计功能,或者用于分析用户对应用程序的使用习惯等。我们还可以通过比较不同应用程序的使用时长,来对用户的偏好进行分析,从而优化用户体验。 需要注意的是,为了使用该方法,我们需要在AndroidManifest.xml文件中添加权限声明:android.permission.PACKAGE_USAGE_STATS。用户在安装应用程序时,也需要授权给应用程序获取应用使用统计信息的权限。 总之,UsageStatsManager queryUsageStats()方法提供了一种获取应用程序使用统计信息的功能,为我们开发与应用程序使用情况相关的功能和分析提供了便利。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值