You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

如何不借助无障碍服务,仅通过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);
}

适配与优化要点

  1. 厂商适配:不同品牌的系统对Overview的处理不一样,比如有些厂商的Overview会让com.android.systemui成为前台,有些则不会,需要针对不同机型测试调整判断规则。
  2. 误判规避
    • 排除屏幕锁屏/熄灭的场景:可以结合PowerManager判断屏幕状态,锁屏时直接跳过检测
    • 调整延迟确认的时间(比如100-200ms),避免把“快速退出应用”误判为打开Overview
  3. 耗电优化:轮询间隔不要太短,100-200ms足够,后台运行时可以用WorkManager结合电池优化策略,避免被系统杀死。

局限性说明

这种方法没法做到100%精准,因为:

  • 部分定制系统会修改Overview的实现逻辑,导致UsageStats里的记录不符合我们的判断规则
  • 极端场景下(比如用户快速切换应用又马上打开Overview)可能出现误判
    但在不能用AccessibilityService的前提下,这是目前最可行的方案了,只要做好适配,基本能覆盖大部分主流机型的需求。

火山引擎 最新活动