如何不借助无障碍服务,仅通过UsageStatsManager检测「最近应用」菜单的打开?
如何不借助无障碍服务,仅通过UsageStatsManager检测「最近应用」菜单的打开?
这个需求我之前帮朋友做AppLocker类应用时碰到过,不用AccessibilityService的话,靠UsageStatsManager确实能实现,但得摸清楚它的脾气,还要做不少适配工作——毕竟最近应用(Overview)界面本身不是普通的第三方应用,没法直接靠包名精准捕捉。
核心思路:通过前台应用的状态变化间接推断
最近应用界面属于系统核心组件,不会像普通App那样在UsageStats里留下明确的“前台应用”记录。但用户打开Overview时,系统会把当前前台应用移到后台,且短时间内不会有新的第三方应用/桌面应用成为前台——我们就靠这个特征来间接判断。
具体实现步骤
1. 先搞定权限前提
首先确保已经拿到PACKAGE_USAGE_STATS权限,这个是特殊权限,没法通过代码直接申请,必须引导用户到系统设置的「应用使用权限」里手动开启你的应用权限。
2. 持续监控前台应用的变化
推荐用定时轮询的方式(比监听UsageEvents更省电,适合后台长期运行),每隔100-200ms查询一次最近的使用统计:
- 调用
UsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, 起始时间, 当前时间),获取最近1秒内的使用记录 - 遍历统计列表,找到最后一次处于前台的应用包名
3. 关键判断逻辑
我们需要区分三种场景:打开新应用、回到桌面、打开Overview,核心判断规则如下:
- 先记录上一次的前台应用包名(比如
mLastForegroundPackage) - 当轮询发现
mLastForegroundPackage不再是前台,且:- 新的前台应用是桌面包名 → 判定为回到桌面,忽略
- 没有新的前台应用(或新前台是
com.android.systemui),且这种状态持续100ms以上 → 判定为打开了Overview
4. 代码示例(简化版)
// 初始化UsageStatsManager UsageStatsManager usageStatsManager = (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE); // 记录上一次的前台包名,建议存在全局变量里 private String mLastForegroundPackage; // 轮询任务(可以用Handler或WorkManager实现后台轮询) private void checkForegroundApp() { long now = System.currentTimeMillis(); // 查询最近1秒的使用统计 List<UsageStats> statsList = usageStatsManager.queryUsageStats( UsageStatsManager.INTERVAL_BEST, now - 1000, now ); // 找到最新的前台应用 String currentForeground = null; long latestActiveTime = 0; for (UsageStats stats : statsList) { // 只统计有前台运行时间的记录 if (stats.getTotalTimeInForeground() > 0 && stats.getLastTimeUsed() > latestActiveTime) { latestActiveTime = stats.getLastTimeUsed(); currentForeground = stats.getPackageName(); } } // 对比上一次的记录 if (mLastForegroundPackage != null && !mLastForegroundPackage.equals(currentForeground)) { // 先排除回到桌面的情况 if (isLauncherPackage(currentForeground)) { // 是桌面,更新记录后跳过 mLastForegroundPackage = currentForeground; return; } // 延迟100ms再确认一次,避免误判(比如快速按Back键退出应用的情况) new Handler(Looper.getMainLooper()).postDelayed(() -> { long checkNow = System.currentTimeMillis(); List<UsageStats> checkStats = usageStatsManager.queryUsageStats( UsageStatsManager.INTERVAL_BEST, checkNow - 200, checkNow ); String checkForeground = null; long checkLatestTime = 0; for (UsageStats s : checkStats) { if (s.getTotalTimeInForeground() > 0 && s.getLastTimeUsed() > checkLatestTime) { checkLatestTime = s.getLastTimeUsed(); checkForeground = s.getPackageName(); } } // 两种情况判定为Overview激活: // 1. 仍然没有新的前台应用 // 2. 前台应用是系统UI(部分厂商的Overview会以SystemUI作为前台) if (checkForeground == null || checkForeground.equals("com.android.systemui")) { // 这里触发你的Overlay显示逻辑 showLockOverlay(); } }, 100); } // 更新上一次的前台包名记录 if (currentForeground != null) { mLastForegroundPackage = currentForeground; } } // 辅助函数:判断是否是桌面应用包名 private boolean isLauncherPackage(String packageName) { if (packageName == null) return false; // 可以根据需要添加更多厂商的桌面包名 List<String> launcherList = Arrays.asList( "com.android.launcher3", // 原生/大部分类原生系统 "com.miui.home", // 小米/红米 "com.huawei.android.launcher", // 华为/荣耀 "com.sec.android.app.launcher", // 三星 "com.coloros.launcher" // OPPO ); return launcherList.contains(packageName); }
适配与优化要点
- 厂商适配:不同品牌的系统对Overview的处理不一样,比如有些厂商的Overview会让
com.android.systemui成为前台,有些则不会,需要针对不同机型测试调整判断规则。 - 误判规避:
- 排除屏幕锁屏/熄灭的场景:可以结合
PowerManager判断屏幕状态,锁屏时直接跳过检测 - 调整延迟确认的时间(比如100-200ms),避免把“快速退出应用”误判为打开Overview
- 排除屏幕锁屏/熄灭的场景:可以结合
- 耗电优化:轮询间隔不要太短,100-200ms足够,后台运行时可以用
WorkManager结合电池优化策略,避免被系统杀死。
局限性说明
这种方法没法做到100%精准,因为:
- 部分定制系统会修改Overview的实现逻辑,导致UsageStats里的记录不符合我们的判断规则
- 极端场景下(比如用户快速切换应用又马上打开Overview)可能出现误判
但在不能用AccessibilityService的前提下,这是目前最可行的方案了,只要做好适配,基本能覆盖大部分主流机型的需求。




