通过 Android 辅助功能「Accessibility Service」 检测任意前台界面

Posted on 2016-02-03 by Shawn Wang

Posted in android

前两周看到大牛 @闭关写代码 发微博总结了在 Android 里判断 APP 是否处于前台的方法总结, 并把它们整合成了一个工具库 AndroidProcess。 我看了项目觉得总结得非常全面, 其中读取 /proc 进程信息的方法我们挺久以前就有所利用,确实算是一定意义上的“黑科技”。 @闭关写代码 已经总结的方案如下:

foreground method list

我转发了微博并说 “......其实我知道的还有方法六......”,昨天 @闭关写代码 询问我第六种方法, 这让我觉得也不应该私藏,还是把这些干货抖出来吧。

Android 辅助功能(AccessibilityService) 为我们提供了一系列的事件回调,帮助我们指示一些用户界面的状态变化。 我们可以派生辅助功能类,进而对不同的 AccessibilityEvent 进行处理。 同样的,这个服务就可以用来判断当前的前台应用,这就是我所谓的“方法6”。

优势

  • AccessibilityService 有非常广泛的 ROM 覆盖,特别是非国产手机,从 Android API Level 18(Android 2.2) 到 Android Api Level 23(Android 6.0)
  • AccessibilityService 不再需要轮询的判断当前的应用是不是在前台,系统会在窗口状态发生变化的时候主动回调,耗时和资源消耗都极小
  • 不需要权限请求
  • 它是一个稳定的方法,与 “方法5”读取 /proc 目录不同,它并非利用 Android 一些设计上的漏洞,可以长期使用的可能很大
  • 可以用来判断任意应用甚至 Activity, PopupWindow, Dialog 对象是否处于前台

劣势

  • 需要要用户开启辅助功能
  • 辅助功能会伴随应用被“强行停止”而剥夺

步骤

1. 派生 Accessibility Service,创建窗口状态探测服务

创建 DetectionService.java

/**
 * Created by shawn
 * Data: 2/3/2016
 * Blog: effmx.com
 */
public class DetectionService extends AccessibilityService {

    final static String TAG = "DetectionService";

    static String foregroundPackageName;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return 0; // 根据需要返回不同的语义值
    }


    /**
     * 重载辅助功能事件回调函数,对窗口状态变化事件进行处理
     * @param event
     */
    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            /*
             * 如果 与 DetectionService 相同进程,直接比较 foregroundPackageName 的值即可
             * 如果在不同进程,可以利用 Intent 或 bind service 进行通信
             */
            foregroundPackageName = event.getPackageName().toString();

            /*
             * 基于以下还可以做很多事情,比如判断当前界面是否是 Activity,是否系统应用等,
             * 与主题无关就不再展开。
             */
            ComponentName cName = new ComponentName(event.getPackageName().toString(),
                    event.getClassName().toString());
        }
    }

    @Override
    public void onInterrupt() {
    }

    @Override
    protected  void onServiceConnected() {
        super.onServiceConnected();
    }
}

2. 创建 Accessibility Service Info 属性文件

创建 res/xml/detection_service_config.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- 根据的 Service 的不同功能需要,你可能需要不同的配置 -->
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeWindowStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagIncludeNotImportantViews" />

3. 注册 Detection Service 到 AndroidManifest.xml

在 AndroidManifest.xml 里添加

<service
    android:name="your_package.DetectionService"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">

    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService"/>
    </intent-filter>

    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/detection_service_config"/>

</service>

4. 使用 Detection Service 判断应用是否在前台

创建 isForegroundPkgViaDetectionService() 函数

    /**
     * 方法6:使用 Android AccessibilityService 探测窗口变化,跟据系统回传的参数获取 前台对象 的包名与类名
     *
     * @param packageName 需要检查是否位于栈顶的App的包名
     */
    public static boolean isForegroundPkgViaDetectionService(String packageName) {
        return packageName.equals(DetectingService.foregroundPackageName);
    }

记得去设置里开启辅助功能,现在你就可以通过 isForegroundPkgViaDetectService() 判断应用是否在前台了,只需要传入相应应用的包名为参数即可。

当然,你也可以参照以下方式引导用户开启辅助功能↓

引导用户开启辅助功能

    final static String TAG = "AccessibilityUtil";

    // 此方法用来判断当前应用的辅助功能服务是否开启
    public static boolean isAccessibilitySettingsOn(Context context) {
        int accessibilityEnabled = 0;
        try {
            accessibilityEnabled = Settings.Secure.getInt(context.getContentResolver(),
                    android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
        } catch (Settings.SettingNotFoundException e) {
            Log.i(TAG, e.getMessage());
        }

        if (accessibilityEnabled == 1) {
            String services = Settings.Secure.getString(context.getContentResolver(),
                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
            if (services != null) {
                return services.toLowerCase().contains(context.getPackageName().toLowerCase());
            }
        }

        return false;
    }

    private void anyMethod() {
        // 判断辅助功能是否开启
        if (!isAccessibilitySettingsOn(getContext())) {
            // 引导至辅助功能设置页面
            startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
        } else {
            // 执行辅助功能服务相关操作
        }
    }

效果如下:

引导配置
我 Fork 了 wenmingvs/AndroidProcess 项目,并为其添加了“方法6”, 想要看到全部代码的同学可以移步我的 AndroidProcess

其他的一点讨论

在很多场景,我们想要判断并不仅仅是应用本身是不是处于前台,而是想要知道其他的应用是否处于前台(比如:当前是否在桌面,是否在拔打电话)。

  • 方法1就不讨论了,其在 Android 5.0 以上已经无法使用;
  • 方法2在 Android M 发布以后也做出了限制,对于非系统应用 ,getRunningAppProcesses() 方法已经只能获取到自身应用和桌面应用的进程信息了,而这种限制在后来又被合并到 Android 5.1(甚至一些 Android 5.0)版本,这也是我为什么觉得“方法5”是“黑科技”的原因,因为它解决了太多的问题;
  • 方法3也只能用来判断自身应用。
  • 方法4能解决这个问题,也确实是最符合Google意图的方式,不过在使用时会有一些延时需要小心处理。
  • 方法5确实是黑科技,他甚至帮助我们在某些功能上走在了国内很多大厂以前,但如果只用来实现这个功能话,频繁的文件 IO 操作实在并非明智之举。

对于判断任意界面是否在前台,这里提出的“方法6”确实是一个不错的选择。

请留意:
辅助功能是 Android 用以辅助用户与手机的交互行为,他提供了很多的功能,也提供了很多其它的可能性。希望大家 不要滥用!不要滥用!不要滥用! 做一个 Android 平台上的良好公民,共同维护好大家的生态环境。


android 辅助功能 Accessibility Service

Donation

Latest Posts

在 VPS 上搭建 Cisco IPsec|L2TP over IPsec 的极简攻略

三年前我写过一篇在VPS上搭建PPTP VPN的极简攻略, 不过一年前我就不再使用 PPTP VPN 了,最主要的原因是因为 macOS 完全不支持 PPTP;另一个原因是基于 ipsec 协议的 VPN 更加安全,IPsec 协议会加密你的网络数据, 避免泄漏或者中间人攻击。所以现在对于需要全局代

为什么应该使用本地广播(LocalBroadcastManager)

从 Android 诞生已来,就一直有所谓的四大组件,BroadcastReceiver 是其中之一。 几乎在各种样的应用中都有 BroadcastReceiver 的使用,它被应用于接收系统发送的消息以及与其他应用之间的交互,但也被大量的误用于应用内部通信。 然而在同应用中使用则违背 Broadc

推荐 Vocabulary.com

阅读之前如果你还在思考背单词的意义,我建议你先想清楚,或者参考别人的意见,例如知乎的讨论 背单词是必须吗 等问题。 从英语方面来说,我肯定不是大神,小神都算不上。 我背单词的路径基本是 中学大学英语书附录 -> 高频词汇书 -> 扇贝单词 -> Vocabulary.com。 那为什么要来推荐 Vo

Comments